From 7a1864c670b0e6cb7a779f71198f5bfcd9c963e1 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 28 Jun 2024 22:01:03 +0300 Subject: [PATCH 001/105] add android loader --- Runtime/LLMLib.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Runtime/LLMLib.cs b/Runtime/LLMLib.cs index 45fc4260..95cd5576 100644 --- a/Runtime/LLMLib.cs +++ b/Runtime/LLMLib.cs @@ -104,6 +104,8 @@ public static IntPtr LoadLibrary(string libraryName) handle = Linux.dlopen(libraryName); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) handle = Mac.dlopen(libraryName); + else if (Application.platform == RuntimePlatform.Android) + handle = Android.dlopen(libraryName); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to load library '{libraryName}'."); @@ -122,6 +124,8 @@ public static IntPtr GetSymbol(IntPtr library, string symbolName) handle = Linux.dlsym(library, symbolName); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) handle = Mac.dlsym(library, symbolName); + else if (Application.platform == RuntimePlatform.Android) + handle = Android.dlsym(library, symbolName); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to load symbol '{symbolName}' from library {library}."); @@ -139,6 +143,8 @@ public static void FreeLibrary(IntPtr library) Linux.dlclose(library); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) Mac.dlclose(library); + else if (Application.platform == RuntimePlatform.Android) + Android.dlclose(library); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to close library '{library}'."); } @@ -231,6 +237,22 @@ private static class Win32 [DllImport(SystemLibrary, SetLastError = true, CharSet = CharSet.Ansi)] public static extern void FreeLibrary(IntPtr hModule); } + + private static class Android + { + public static IntPtr dlopen(string path) => dlopen(path, 1); + // LoadLibrary for Android + [DllImport("__Internal")] + public static extern IntPtr dlopen(string filename, int flags); + + // GetSymbol for Android + [DllImport("__Internal")] + public static extern IntPtr dlsym(IntPtr handle, string symbol); + + // FreeLibrary for Android + [DllImport("__Internal")] + public static extern int dlclose(IntPtr handle); + } } public class LLMLib @@ -334,6 +356,10 @@ public static List PossibleArchitectures(bool gpu = false) architectures.Add("x64-no_acc"); } } + else if (Application.platform == RuntimePlatform.Android) + { + architectures.Add("android"); + } else { string error = "Unknown OS"; @@ -376,6 +402,10 @@ public static string GetArchitecturePath(string arch) { filename = $"macos-{arch}/libundreamai_macos-{arch}.dylib"; } + else if (Application.platform == RuntimePlatform.Android) + { + return "libundreamai_android_plugin.so"; + } else { string error = "Unknown OS"; From 7414d95c0d355752747c5bf3faef4984a6e8dd78 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 28 Jun 2024 22:03:04 +0300 Subject: [PATCH 002/105] add android postprocessor --- Editor/LLMBuildProcessor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index d0362ead..a1ebecac 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -54,7 +54,7 @@ public void BuildCompleted() static List GetLibraryPlatformsToHide(BuildTarget platform) { - List platforms = new List(){ "windows", "macos", "linux" }; + List platforms = new List(){ "windows", "macos", "linux", "android" }; switch (platform) { case BuildTarget.StandaloneWindows: @@ -67,6 +67,9 @@ static List GetLibraryPlatformsToHide(BuildTarget platform) case BuildTarget.StandaloneOSX: platforms.Remove("macos"); break; + case BuildTarget.Android: + platforms.Remove("android"); + break; } return platforms; } From 3dc86a81f9d5dc02c0ea909400f8e946d2adcfcd Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 12 Jul 2024 20:11:15 +0300 Subject: [PATCH 003/105] function to determine the number of big cores in Android --- Runtime/LLMUnitySetup.cs | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 74022dbc..0a4c7dd4 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -8,6 +8,11 @@ using System; using System.IO.Compression; using System.Collections.Generic; +<<<<<<< HEAD +======= +using UnityEngine.Networking; +using System.Collections.Generic; +>>>>>>> d9fdc86 (function to determine the number of big cores in Android) /// @defgroup llm LLM /// @defgroup template Chat Templates @@ -290,5 +295,49 @@ public static void DownloadModel(LLM llm, int optionIndex) #endif /// \endcond + + /// + /// Calculates the number of big cores in Android based on https://docs.unity3d.com/2022.3/Documentation/Manual/android-thread-configuration.html + /// + /// + public static int AndroidGetNumBigCores() + { + List capacities = new List(); + int minCapacity = int.MaxValue; + try + { + string cpuPath = "/sys/devices/system/cpu/"; + int coreIndex; + if (Directory.Exists(cpuPath)) + { + foreach (string cpuDir in Directory.GetDirectories(cpuPath)) + { + string dirName = Path.GetFileName(cpuDir); + if (!dirName.StartsWith("cpu")) continue; + if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue; + + string capacityPath = Path.Combine(cpuDir, "cpu_capacity"); + if (!File.Exists(capacityPath)) break; + + int capacity = int.Parse(File.ReadAllText(capacityPath).Trim()); + capacities.Add(capacity); + if (minCapacity > capacity) minCapacity = capacity; + } + } + } + catch (Exception e) + { + Debug.LogError(e.Message); + } + + int numBigCores = 0; + foreach (int capacity in capacities) + { + if (capacity >= 2 * minCapacity) numBigCores++; + } + + if (numBigCores == 0) numBigCores = SystemInfo.processorCount; + return numBigCores; + } } } From 699e902b7cf9afe1d3c2657019547e15e88f6067 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 12 Jul 2024 20:11:57 +0300 Subject: [PATCH 004/105] use the number of big cores if on Android --- Runtime/LLM.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 74e51e33..8dc8d475 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -196,10 +196,13 @@ protected virtual string GetLlamaccpArguments() } } + int numThreadsToUse = numThreads; + if (Application.platform == RuntimePlatform.Android && numThreads <= 0) numThreadsToUse = LLMUnitySetup.AndroidGetNumBigCores(); + int slots = GetNumClients(); string arguments = $"-m \"{modelPath}\" -c {contextSize} -b {batchSize} --log-disable -np {slots}"; if (remote) arguments += $" --port {port} --host 0.0.0.0"; - if (numThreads > 0) arguments += $" -t {numThreads}"; + if (numThreadsToUse > 0) arguments += $" -t {numThreadsToUse}"; if (loraPath != "") arguments += $" --lora \"{loraPath}\""; arguments += $" -ngl {numGPULayers}"; return arguments; From b6df39fe0bb0d13b440eca84026880872cab6bdf Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 12 Jul 2024 20:39:25 +0300 Subject: [PATCH 005/105] cap big cores at processorCount --- Runtime/LLMUnitySetup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 0a4c7dd4..2d4df4f8 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -336,7 +336,7 @@ public static int AndroidGetNumBigCores() if (capacity >= 2 * minCapacity) numBigCores++; } - if (numBigCores == 0) numBigCores = SystemInfo.processorCount; + if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount; return numBigCores; } } From 40b8a5d6e0509795b4217d3a8c751a48b72eae0c Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 17 Jul 2024 13:20:39 +0300 Subject: [PATCH 006/105] detect big cores based on max frequency --- Runtime/LLMUnitySetup.cs | 115 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 2d4df4f8..73d32c1f 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -295,12 +295,125 @@ public static void DownloadModel(LLM llm, int optionIndex) #endif /// \endcond + public static int GetMaxFreqKHz(int cpuId) + { + string[] paths = new string[] + { + $"/sys/devices/system/cpu/cpufreq/stats/cpu{cpuId}/time_in_state", + $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/stats/time_in_state", + $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/cpuinfo_max_freq" + }; + + foreach (var path in paths) + { + if (!File.Exists(path)) continue; + + int maxFreqKHz = 0; + using (StreamReader sr = new StreamReader(path)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + string[] parts = line.Split(' '); + if (parts.Length > 0 && int.TryParse(parts[0], out int freqKHz)) + { + if (freqKHz > maxFreqKHz) + { + maxFreqKHz = freqKHz; + } + } + } + } + if (maxFreqKHz != 0) return maxFreqKHz; + } + return -1; + } + + public static bool IsSmtCpu(int cpuId) + { + string[] paths = new string[] + { + $"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list", + $"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list" + }; + + foreach (var path in paths) + { + if (!File.Exists(path)) continue; + using (StreamReader sr = new StreamReader(path)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + if (line.Contains(",") || line.Contains("-")) + { + return true; + } + } + } + } + return false; + } /// - /// Calculates the number of big cores in Android based on https://docs.unity3d.com/2022.3/Documentation/Manual/android-thread-configuration.html + /// Calculates the number of big cores in Android similarly to ncnn (https://github.com/Tencent/ncnn) /// /// public static int AndroidGetNumBigCores() + { + int maxFreqKHzMin = int.MaxValue; + int maxFreqKHzMax = 0; + List cpuMaxFreqKHz = new List(); + List cpuIsSmtCpu = new List(); + + try + { + string cpuPath = "/sys/devices/system/cpu/"; + int coreIndex; + if (Directory.Exists(cpuPath)) + { + foreach (string cpuDir in Directory.GetDirectories(cpuPath)) + { + string dirName = Path.GetFileName(cpuDir); + if (!dirName.StartsWith("cpu")) continue; + if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue; + + int maxFreqKHz = GetMaxFreqKHz(coreIndex); + cpuMaxFreqKHz.Add(maxFreqKHz); + if (maxFreqKHz > maxFreqKHzMax) maxFreqKHzMax = maxFreqKHz; + if (maxFreqKHz < maxFreqKHzMin) maxFreqKHzMin = maxFreqKHz; + cpuIsSmtCpu.Add(IsSmtCpu(coreIndex)); + } + } + } + catch (Exception e) + { + Debug.LogError(e.Message); + } + + int numBigCores = 0; + int numCores = SystemInfo.processorCount; + int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2; + if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores; + else + { + for (int i = 0; i < cpuMaxFreqKHz.Count; i++) + { + if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++; + } + } + + if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2; + else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount); + + return numBigCores; + } + + /// + /// Calculates the number of big cores in Android similarly to Unity (https://docs.unity3d.com/2022.3/Documentation/Manual/android-thread-configuration.html) + /// + /// + public static int AndroidGetNumBigCoresCapacity() { List capacities = new List(); int minCapacity = int.MaxValue; From f105b5ec520a0be10ec70396c0768206db1c761e Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 17 Jul 2024 13:42:39 +0300 Subject: [PATCH 007/105] move arguments to Awake --- Runtime/LLM.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 8dc8d475..24f376e9 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -215,8 +215,10 @@ protected virtual string GetLlamaccpArguments() public async void Awake() { if (!enabled) return; - if (asynchronousStartup) await Task.Run(() => StartLLMServer()); - else StartLLMServer(); + string arguments = GetLlamaccpArguments(); + if (arguments == null) return; + if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); + else StartLLMServer(arguments); if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); if (basePrompt != "") await SetBasePrompt(basePrompt); } @@ -234,12 +236,10 @@ private void StopLogging() DestroyStreamWrapper(logStreamWrapper); } - private void StartLLMServer() + private void StartLLMServer(string arguments) { started = false; failed = false; - string arguments = GetLlamaccpArguments(); - if (arguments == null) return; bool useGPU = numGPULayers > 0; LLMUnitySetup.Log($"Server command: {arguments}"); From 3d1d6ce9550459eaea4d0393841264aaf4930917 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 17 Jul 2024 23:52:26 +0300 Subject: [PATCH 008/105] android demo sample --- Samples~/AndroidDemo.meta | 8 + Samples~/AndroidDemo/AndroidDemo.cs | 106 + Samples~/AndroidDemo/AndroidDemo.cs.meta | 11 + Samples~/AndroidDemo/Scene.unity | 2261 ++++++++++++++++++++++ Samples~/AndroidDemo/Scene.unity.meta | 7 + 5 files changed, 2393 insertions(+) create mode 100644 Samples~/AndroidDemo.meta create mode 100644 Samples~/AndroidDemo/AndroidDemo.cs create mode 100644 Samples~/AndroidDemo/AndroidDemo.cs.meta create mode 100644 Samples~/AndroidDemo/Scene.unity create mode 100644 Samples~/AndroidDemo/Scene.unity.meta diff --git a/Samples~/AndroidDemo.meta b/Samples~/AndroidDemo.meta new file mode 100644 index 00000000..9b3afc06 --- /dev/null +++ b/Samples~/AndroidDemo.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4259a324b4c9af10a95ced582b171297 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs new file mode 100644 index 00000000..7424a145 --- /dev/null +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -0,0 +1,106 @@ +using UnityEngine; +using LLMUnity; +using UnityEngine.UI; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LLMUnitySamples +{ + public class AndroidDemo : MonoBehaviour + { + public LLM llm; + public LLMCharacter llmCharacter; + + public GameObject ChatPanel; + public InputField playerText; + public Text AIText; + + public GameObject DownloadPanel; + public Scrollbar progressBar; + public Text progressText; + int cores; + + void Awake() + { + ChatPanel.SetActive(false); + DownloadPanel.SetActive(false); + } + + void Start() + { + playerText.onSubmit.AddListener(onInputFieldSubmit); + playerText.interactable = false; + StartCoroutine(Loading()); + } + + IEnumerator Loading() + { + DownloadPanel.SetActive(true); + AIText.text = "Downloading model..."; + Task downloadTask = llm.DownloadModel( + "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true", + null, + SetProgress, + true + ); + while (!downloadTask.IsCompleted) yield return null; + DownloadPanel.SetActive(false); + + ChatPanel.SetActive(true); + cores = LLMUnitySetup.AndroidGetNumBigCores(); + AIText.text += $"\nWarming up the model...\nWill use {cores} cores"; + Task warmup = llmCharacter.Warmup(); + while (!warmup.IsCompleted) yield return null; + + AIText.text = $"Ready when you are ({cores} cores)!"; + AIReplyComplete(); + } + + void SetProgress(float progress) + { + progressText.text = ((int)(progress * 100)).ToString() + "%"; + progressBar.size = progress; + } + + void onInputFieldSubmit(string message) + { + playerText.interactable = false; + AIText.text = "..."; + _ = llmCharacter.Chat(message, SetAIText, AIReplyComplete); + } + + public void SetAIText(string text) + { + AIText.text = text; + } + + public void AIReplyComplete() + { + playerText.interactable = true; + playerText.Select(); + playerText.text = ""; + } + + public void CancelRequests() + { + llmCharacter.CancelRequests(); + AIReplyComplete(); + } + + public void ExitGame() + { + Debug.Log("Exit button clicked"); + Application.Quit(); + } + + bool onValidateWarning = true; + void OnValidate() + { + if (onValidateWarning && !llmCharacter.remote && llmCharacter.llm != null && llmCharacter.llm.model == "") + { + Debug.LogWarning($"Please select a model in the {llmCharacter.llm.gameObject.name} GameObject!"); + onValidateWarning = false; + } + } + } +} diff --git a/Samples~/AndroidDemo/AndroidDemo.cs.meta b/Samples~/AndroidDemo/AndroidDemo.cs.meta new file mode 100644 index 00000000..3409ae47 --- /dev/null +++ b/Samples~/AndroidDemo/AndroidDemo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 708542fec6999ea3ebd7c69404932bb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity new file mode 100644 index 00000000..9e404a5d --- /dev/null +++ b/Samples~/AndroidDemo/Scene.unity @@ -0,0 +1,2261 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657832, g: 0.49641222, b: 0.57481664, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &107963744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 107963746} + - component: {fileID: 107963747} + m_Layer: 0 + m_Name: AndroidDemo + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &107963746 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 107963744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &107963747 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 107963744} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 708542fec6999ea3ebd7c69404932bb3, type: 3} + m_Name: + m_EditorClassIdentifier: + llm: {fileID: 1047848254} + llmCharacter: {fileID: 498662973} + ChatPanel: {fileID: 1084608230} + playerText: {fileID: 1966107897} + AIText: {fileID: 887085510} + DownloadPanel: {fileID: 332743750} + progressBar: {fileID: 332743752} + progressText: {fileID: 381203299} +--- !u!1 &158550913 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 158550917} + - component: {fileID: 158550916} + - component: {fileID: 158550915} + - component: {fileID: 158550914} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &158550914 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &158550915 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 720, y: 720} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0.5 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &158550916 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &158550917 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 268891590} + - {fileID: 332743751} + - {fileID: 1084608231} + - {fileID: 1308766010} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &268891589 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 268891590} + - component: {fileID: 268891592} + - component: {fileID: 268891591} + m_Layer: 5 + m_Name: Panel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &268891590 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.01, y: 1.01, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &268891591 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &268891592 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_CullTransparentMesh: 1 +--- !u!1 &332743750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 332743751} + - component: {fileID: 332743754} + - component: {fileID: 332743753} + - component: {fileID: 332743752} + m_Layer: 5 + m_Name: DownloadPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &332743751 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1688602497} + - {fileID: 1705264488} + - {fileID: 381203298} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -34.8} + m_SizeDelta: {x: 160, y: 18} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &332743752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 659217392} + m_HandleRect: {fileID: 659217391} + m_Direction: 0 + m_Value: 0 + m_Size: 0 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &332743753 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.5207577, g: 0.5207577, b: 0.5207577, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &332743754 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_CullTransparentMesh: 1 +--- !u!1 &381203297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 381203298} + - component: {fileID: 381203300} + - component: {fileID: 381203299} + m_Layer: 5 + m_Name: Progress + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &381203298 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 5, y: 0} + m_SizeDelta: {x: 79.6491, y: 19.2105} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &381203299 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0% +--- !u!222 &381203300 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_CullTransparentMesh: 1 +--- !u!1 &433287079 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 433287080} + - component: {fileID: 433287082} + - component: {fileID: 433287081} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &433287080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 724531320} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &433287081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'Stop + +' +--- !u!222 &433287082 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_CullTransparentMesh: 1 +--- !u!1 &498662970 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 498662972} + - component: {fileID: 498662973} + m_Layer: 0 + m_Name: LLMCharacter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &498662972 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 498662970} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1024.2354, y: 495.014, z: -3.4752133} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &498662973 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 498662970} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3f6c87a428fd5d0be9bbc686bdc8c3c2, type: 3} + m_Name: + m_EditorClassIdentifier: + advancedOptions: 0 + remote: 0 + llm: {fileID: 1047848254} + host: localhost + port: 13333 + save: + saveCache: 0 + debugPrompt: 0 + stream: 1 + grammar: + cachePrompt: 1 + seed: 0 + numPredict: 256 + temperature: 0.2 + topK: 40 + topP: 0.9 + minP: 0.05 + repeatPenalty: 1.1 + presencePenalty: 0 + frequencyPenalty: 0 + tfsZ: 1 + typicalP: 1 + repeatLastN: 64 + penalizeNl: 1 + penaltyPrompt: + mirostat: 0 + mirostatTau: 5 + mirostatEta: 0.1 + nProbs: 0 + ignoreEos: 0 + nKeep: -1 + stop: [] + playerName: user + AIName: assistant + prompt: A chat between a curious human and an artificial intelligence assistant. + The assistant gives helpful, detailed, and polite answers to the human's questions. + setNKeepToPrompt: 1 + chat: [] + grammarString: +--- !u!1 &659217390 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 659217391} + - component: {fileID: 659217393} + - component: {fileID: 659217392} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &659217391 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1705264488} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &659217392 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &659217393 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_CullTransparentMesh: 1 +--- !u!1 &724531319 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 724531320} + - component: {fileID: 724531323} + - component: {fileID: 724531322} + - component: {fileID: 724531321} + m_Layer: 5 + m_Name: StopButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &724531320 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 433287080} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 10.624025, y: -80.006996} + m_SizeDelta: {x: 107.0426, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &724531321 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 724531322} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 0} + m_TargetAssemblyTypeName: SimpleInteraction, Assembly-CSharp + m_MethodName: CancelRequests + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 +--- !u!114 &724531322 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &724531323 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_CullTransparentMesh: 1 +--- !u!1 &726528676 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 726528679} + - component: {fileID: 726528678} + - component: {fileID: 726528677} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &726528677 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + m_Enabled: 1 +--- !u!20 &726528678 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &726528679 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &856480601 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 856480602} + - component: {fileID: 856480604} + - component: {fileID: 856480603} + m_Layer: 5 + m_Name: Player title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &856480602 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 10.624025, y: 62.99302} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &856480603 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 20 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Player +--- !u!222 &856480604 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_CullTransparentMesh: 1 +--- !u!1 &887085508 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 887085509} + - component: {fileID: 887085511} + - component: {fileID: 887085510} + - component: {fileID: 887085512} + m_Layer: 5 + m_Name: AIText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &887085509 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2091685447} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &887085510 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!222 &887085511 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_CullTransparentMesh: 1 +--- !u!210 &887085512 +SortingGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_Enabled: 1 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 1 + m_SortAtRoot: 0 +--- !u!1 &909474451 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 909474453} + - component: {fileID: 909474452} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &909474452 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 909474451} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &909474453 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 909474451} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1047848253 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1047848255} + - component: {fileID: 1047848254} + m_Layer: 0 + m_Name: LLM + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1047848254 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1047848253} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a50e3140c3ecaaf1c848dbf141cc2074, type: 3} + m_Name: + m_EditorClassIdentifier: + advancedOptions: 0 + remote: 0 + port: 13333 + numThreads: -1 + numGPULayers: 0 + debug: 1 + parallelPrompts: -1 + asynchronousStartup: 1 + dontDestroyOnLoad: 1 + model: smol_llama-220m-openhermes.q4_k_m.gguf + lora: + contextSize: 0 + batchSize: 512 + basePrompt: + SelectedModel: 0 + modelProgress: 1 + modelCopyProgress: 1 + modelHide: 1 + chatTemplate: chatml +--- !u!4 &1047848255 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1047848253} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1059033619 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1059033620} + - component: {fileID: 1059033622} + - component: {fileID: 1059033621} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1059033620 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1966107896} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1059033621 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 2 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Enter text... +--- !u!222 &1059033622 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_CullTransparentMesh: 1 +--- !u!1 &1084608230 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1084608231} + m_Layer: 5 + m_Name: ChatPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1084608231 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1084608230} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1966107896} + - {fileID: 2091685447} + - {fileID: 856480602} + - {fileID: 1342801405} + - {fileID: 724531320} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 87.00699} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1171567417 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1171567418} + - component: {fileID: 1171567420} + - component: {fileID: 1171567419} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1171567418 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1308766010} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 2} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1171567419 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\xD7" +--- !u!222 &1171567420 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_CullTransparentMesh: 1 +--- !u!1 &1308766009 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1308766010} + - component: {fileID: 1308766013} + - component: {fileID: 1308766012} + - component: {fileID: 1308766011} + m_Layer: 5 + m_Name: ExitButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1308766010 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1171567418} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 1, y: 1} +--- !u!114 &1308766011 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1308766012} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 0} + m_TargetAssemblyTypeName: SimpleInteraction, Assembly-CSharp + m_MethodName: ExitGame + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 1 +--- !u!114 &1308766012 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.24722636, g: 0.24722636, b: 0.24722636, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1308766013 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_CullTransparentMesh: 1 +--- !u!1 &1342801404 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1342801405} + - component: {fileID: 1342801407} + - component: {fileID: 1342801406} + m_Layer: 5 + m_Name: AI title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1342801405 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 5.6240253, y: -133.00699} + m_SizeDelta: {x: 160, y: 29.6652} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1342801406 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 20 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: AI +--- !u!222 &1342801407 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_CullTransparentMesh: 1 +--- !u!1 &1609985808 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1609985809} + - component: {fileID: 1609985811} + - component: {fileID: 1609985810} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1609985809 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1966107896} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1609985810 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 0 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!222 &1609985811 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_CullTransparentMesh: 1 +--- !u!1 &1688602496 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1688602497} + - component: {fileID: 1688602499} + - component: {fileID: 1688602498} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1688602497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -0.000025749, y: 15.999993} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1688602498 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Downloading model... +--- !u!222 &1688602499 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_CullTransparentMesh: 1 +--- !u!1 &1705264487 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1705264488} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1705264488 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1705264487} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 659217391} + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.0000038146973} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1966107895 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1966107896} + - component: {fileID: 1966107899} + - component: {fileID: 1966107898} + - component: {fileID: 1966107897} + m_Layer: 5 + m_Name: PlayerInput + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1966107896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1059033620} + - {fileID: 1609985809} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -0.000022888184} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1966107897 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1966107898} + m_TextComponent: {fileID: 1609985810} + m_Placeholder: {fileID: 1059033621} + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 1 + m_HideMobileInput: 0 + m_CharacterValidation: 0 + m_CharacterLimit: 0 + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_CustomCaretColor: 0 + m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 +--- !u!114 &1966107898 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1966107899 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_CullTransparentMesh: 1 +--- !u!1 &2015159264 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2015159267} + - component: {fileID: 2015159266} + - component: {fileID: 2015159265} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &2015159265 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &2015159266 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &2015159267 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2091685446 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2091685447} + - component: {fileID: 2091685449} + - component: {fileID: 2091685448} + m_Layer: 5 + m_Name: AIImage + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2091685447 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 887085509} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -197.7609} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2091685448 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.53662825, g: 0.53662825, b: 0.9029823, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2091685449 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_CullTransparentMesh: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 726528679} + - {fileID: 909474453} + - {fileID: 158550917} + - {fileID: 2015159267} + - {fileID: 1047848255} + - {fileID: 498662972} + - {fileID: 107963746} diff --git a/Samples~/AndroidDemo/Scene.unity.meta b/Samples~/AndroidDemo/Scene.unity.meta new file mode 100644 index 00000000..124260ce --- /dev/null +++ b/Samples~/AndroidDemo/Scene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15abb96f71f7fc08db7606aa88332b00 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 0c77e28810c09453b7b791bc17aeb934262e7c97 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 18 Jul 2024 18:27:25 +0300 Subject: [PATCH 009/105] downloader with resume capabilities --- Runtime/ResumingWebClient.cs | 144 ++++++++++++++++++++++++++++++ Runtime/ResumingWebClient.cs.meta | 11 +++ 2 files changed, 155 insertions(+) create mode 100644 Runtime/ResumingWebClient.cs create mode 100644 Runtime/ResumingWebClient.cs.meta diff --git a/Runtime/ResumingWebClient.cs b/Runtime/ResumingWebClient.cs new file mode 100644 index 00000000..cd21f0af --- /dev/null +++ b/Runtime/ResumingWebClient.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LLMUnity +{ + public class ResumingWebClient : WebClient + { + private const int timeoutMs = 30 * 1000; + private SynchronizationContext _context; + private const int DefaultDownloadBufferLength = 65536; + List requests = new List(); + + public ResumingWebClient() + { + _context = SynchronizationContext.Current ?? new SynchronizationContext(); + } + + public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resume = false, Callback progressCallback = null) + { + var tcs = new TaskCompletionSource(address); + FileStream? fs = null; + long bytesToSkip = 0; + + try + { + FileMode filemode = FileMode.Create; + if (resume) + { + var fileInfo = new FileInfo(fileName); + if (fileInfo.Exists) bytesToSkip = fileInfo.Length; + } + + WebRequest request = GetWebRequest(address); + if (request is HttpWebRequest webRequest && bytesToSkip > 0) + { + filemode = FileMode.Append; + LLMUnitySetup.Log($"File exists at {fileName}, skipping {bytesToSkip} bytes"); + webRequest.AddRange(bytesToSkip); + webRequest.ReadWriteTimeout = timeoutMs; + } + + fs = new FileStream(fileName, filemode, FileAccess.Write); + DownloadBitsAsync(request, fs, bytesToSkip, progressCallback, tcs); + } + catch (Exception e) + { + fs?.Close(); + tcs.TrySetException(e); + } + + return tcs.Task; + } + + public void CancelDownloadAsync() + { + LLMUnitySetup.Log("Cancellation requested, aborting download."); + foreach (WebRequest request in requests) AbortRequest(request); + requests.Clear(); + } + + public void AbortRequest(WebRequest request) + { + try + { + request?.Abort(); + } + catch (Exception e) + { + LLMUnitySetup.LogError($"Error aborting request: {e.Message}"); + } + } + + private async void DownloadBitsAsync(WebRequest request, Stream writeStream, long bytesToSkip = 0, Callback progressCallback = null, TaskCompletionSource tcs = null) + { + try + { + requests.Add(request); + WebResponse response = await request.GetResponseAsync().ConfigureAwait(false); + + long contentLength = response.ContentLength; + byte[] copyBuffer = new byte[contentLength == -1 || contentLength > DefaultDownloadBufferLength ? DefaultDownloadBufferLength : contentLength]; + + long TotalBytesToReceive = Math.Max(contentLength, 0) + bytesToSkip; + long BytesReceived = bytesToSkip; + + using (writeStream) + using (Stream readStream = response.GetResponseStream()) + { + if (readStream != null) + { + while (true) + { + int bytesRead = await readStream.ReadAsync(new Memory(copyBuffer)).ConfigureAwait(false); + if (bytesRead == 0) + { + break; + } + + BytesReceived += bytesRead; + if (BytesReceived != TotalBytesToReceive) + { + PostProgressChanged(progressCallback, BytesReceived, TotalBytesToReceive); + } + + await writeStream.WriteAsync(new ReadOnlyMemory(copyBuffer, 0, bytesRead)).ConfigureAwait(false); + } + } + + if (TotalBytesToReceive < 0) + { + TotalBytesToReceive = BytesReceived; + } + PostProgressChanged(progressCallback, BytesReceived, TotalBytesToReceive); + } + tcs.TrySetResult(true); + } + catch (Exception e) + { + tcs.TrySetException(e); + LLMUnitySetup.LogError(e.Message); + AbortRequest(request); + tcs.TrySetResult(false); + } + finally + { + writeStream?.Close(); + requests.Remove(request); + } + } + + private void PostProgressChanged(Callback progressCallback, long BytesReceived, long TotalBytesToReceive) + { + if (progressCallback != null && BytesReceived > 0) + { + float progressPercentage = TotalBytesToReceive < 0 ? 0 : TotalBytesToReceive == 0 ? 1 : (float)BytesReceived / TotalBytesToReceive; + _context.Post(_ => progressCallback?.Invoke(progressPercentage), null); + } + } + } +} diff --git a/Runtime/ResumingWebClient.cs.meta b/Runtime/ResumingWebClient.cs.meta new file mode 100644 index 00000000..369fc67e --- /dev/null +++ b/Runtime/ResumingWebClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00e47c7cca64b8c57ba4b7b6b2c2c5b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From ad68de5a7ecf7b2ed54d8a842ea5f8bd0e5e8829 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 18 Jul 2024 18:28:26 +0300 Subject: [PATCH 010/105] use resume downloader in LLMUnitySetup --- Runtime/LLMUnitySetup.cs | 58 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 73d32c1f..b88ca024 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -4,15 +4,9 @@ using System.IO; using UnityEngine; using System.Threading.Tasks; -using System.Net; using System; using System.IO.Compression; using System.Collections.Generic; -<<<<<<< HEAD -======= -using UnityEngine.Networking; -using System.Collections.Generic; ->>>>>>> d9fdc86 (function to determine the number of big cores in Android) /// @defgroup llm LLM /// @defgroup template Chat Templates @@ -165,71 +159,61 @@ static async Task InitializeOnLoad() await DownloadLibrary(); LoadDebugMode(); } + #else [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] void InitializeOnLoad() { LoadDebugMode(); } + #endif -#if UNITY_EDITOR - [HideInInspector] public static float libraryProgress = 1; + static Dictionary downloadClients = new Dictionary(); - public class DownloadStatus + public static void CancelDownload(string savePath) { - Callback progresscallback; - - public DownloadStatus(Callback progresscallback = null) - { - this.progresscallback = progresscallback; - } - - public void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) - { - progresscallback?.Invoke(e.ProgressPercentage / 100.0f); - } + if (!downloadClients.ContainsKey(savePath)) return; + downloadClients[savePath].CancelDownloadAsync(); + downloadClients.Remove(savePath); } public static async Task DownloadFile( string fileUrl, string savePath, bool overwrite = false, - TaskCallback callback = null, Callback progresscallback = null, - bool async = true + TaskCallback callback = null, Callback progressCallback = null ) { - // download a file to the specified path if (File.Exists(savePath) && !overwrite) { Log($"File already exists at: {savePath}"); } else { - Log($"Downloading {fileUrl}..."); + Log($"Downloading {fileUrl} to {savePath}..."); string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath)); - WebClient client = new WebClient(); - DownloadStatus downloadStatus = new DownloadStatus(progresscallback); - client.DownloadProgressChanged += downloadStatus.DownloadProgressChanged; - if (async) - { - await client.DownloadFileTaskAsync(fileUrl, tmpPath); - } - else - { - client.DownloadFile(fileUrl, tmpPath); - } - + ResumingWebClient client = new ResumingWebClient(); + downloadClients[savePath] = client; + await client.DownloadFileTaskAsyncResume(new Uri(fileUrl), tmpPath, !overwrite, progressCallback); + downloadClients.Remove(savePath); +#if UNITY_EDITOR AssetDatabase.StartAssetEditing(); +#endif Directory.CreateDirectory(Path.GetDirectoryName(savePath)); File.Move(tmpPath, savePath); +#if UNITY_EDITOR AssetDatabase.StopAssetEditing(); +#endif Log($"Download complete!"); } - progresscallback?.Invoke(1f); + progressCallback?.Invoke(1f); if (callback != null) await callback.Invoke(savePath); } +#if UNITY_EDITOR + [HideInInspector] public static float libraryProgress = 1; + public static async Task AddAsset(string assetPath, string basePath) { if (!File.Exists(assetPath)) From f1cd12fd459581a47c75780eb488f48fde54986a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 18 Jul 2024 18:36:01 +0300 Subject: [PATCH 011/105] adapt tests to download changes --- Tests/Runtime/TestLLM.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index 60947fcc..cbb05923 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -32,7 +32,7 @@ public async Task Init() string modelUrl = "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"; string modelPath = "LLMUnityTests/smol_llama-220m-openhermes.q4_k_m.gguf"; string fullModelPath = LLMUnitySetup.GetAssetPath(modelPath); - _ = LLMUnitySetup.DownloadFile(modelUrl, fullModelPath, false, null, null, false); + await LLMUnitySetup.DownloadFile(modelUrl, fullModelPath, false, null, null); await llm.SetModel(fullModelPath); llm.parallelPrompts = 1; llm.SetTemplate("alpaca"); From 19dcc017272a8fa5d6f73d695aab9e74db5a26e9 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 19 Jul 2024 11:26:03 +0300 Subject: [PATCH 012/105] use alpaca template, larger x, connect buttons --- Samples~/AndroidDemo/Scene.unity | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity index 9e404a5d..da2763c6 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -884,8 +884,8 @@ MonoBehaviour: m_OnClick: m_PersistentCalls: m_Calls: - - m_Target: {fileID: 0} - m_TargetAssemblyTypeName: SimpleInteraction, Assembly-CSharp + - m_Target: {fileID: 107963747} + m_TargetAssemblyTypeName: LLMUnitySamples.AndroidDemo, Assembly-CSharp m_MethodName: CancelRequests m_Mode: 1 m_Arguments: @@ -895,7 +895,7 @@ MonoBehaviour: m_FloatArgument: 0 m_StringArgument: m_BoolArgument: 0 - m_CallState: 2 + m_CallState: 0 --- !u!114 &724531322 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1532,10 +1532,10 @@ MonoBehaviour: m_Calls: [] m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} - m_FontSize: 14 + m_FontSize: 20 m_FontStyle: 0 m_BestFit: 0 - m_MinSize: 10 + m_MinSize: 2 m_MaxSize: 40 m_Alignment: 4 m_AlignByGeometry: 0 @@ -1589,7 +1589,7 @@ RectTransform: m_AnchorMin: {x: 1, y: 1} m_AnchorMax: {x: 1, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 25, y: 25} + m_SizeDelta: {x: 35, y: 35} m_Pivot: {x: 1, y: 1} --- !u!114 &1308766011 MonoBehaviour: @@ -1635,8 +1635,8 @@ MonoBehaviour: m_OnClick: m_PersistentCalls: m_Calls: - - m_Target: {fileID: 0} - m_TargetAssemblyTypeName: SimpleInteraction, Assembly-CSharp + - m_Target: {fileID: 107963747} + m_TargetAssemblyTypeName: LLMUnitySamples.AndroidDemo, Assembly-CSharp m_MethodName: ExitGame m_Mode: 1 m_Arguments: From 43d37d0774d3b09ecd5143e95085b2d6818e5cf0 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 19 Jul 2024 11:37:26 +0300 Subject: [PATCH 013/105] move model download to LLM --- Runtime/LLM.cs | 29 +++++++++++++++++++++++++++++ Runtime/LLMUnitySetup.cs | 12 ------------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 24f376e9..e2e8088e 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -122,6 +122,35 @@ async Task CopyAsset(string path) return path; } + public async Task DownloadDefaultModel(int optionIndex) + { + // download default model and disable model editor properties until the model is set + SelectedModel = optionIndex; + string modelUrl = LLMUnitySetup.modelOptions[optionIndex].Item2; + if (modelUrl == null) return; + string modelName = Path.GetFileName(modelUrl).Split("?")[0]; + await DownloadModel(modelUrl, modelName); + } + + public async Task DownloadModel(string modelUrl, string modelName = null, Callback progressCallback = null, bool overwrite = false) + { + modelProgress = 0; + if (modelName == null) modelName = model; + string modelPath = LLMUnitySetup.GetAssetPath(modelName); + + Callback callback = (floatArg) => + { + progressCallback?.Invoke(floatArg); + SetModelProgress(floatArg); + }; + await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, SetModel, callback); + } + + public async Task DownloadModel(string modelUrl, Callback progressCallback = null, bool overwrite = false) + { + await DownloadModel(modelUrl, null, progressCallback, overwrite); + } + /// /// Allows to set the model used by the LLM. /// The model provided is copied to the Assets/StreamingAssets folder that allows it to also work in the build. diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index b88ca024..5b59e1f2 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -265,18 +265,6 @@ private static void SetLibraryProgress(float progress) libraryProgress = progress; } - public static void DownloadModel(LLM llm, int optionIndex) - { - // download default model and disable model editor properties until the model is set - llm.SelectedModel = optionIndex; - string modelUrl = modelOptions[optionIndex].Item2; - if (modelUrl == null) return; - llm.modelProgress = 0; - string modelName = Path.GetFileName(modelUrl).Split("?")[0]; - string modelPath = GetAssetPath(modelName); - Task downloadTask = DownloadFile(modelUrl, modelPath, false, llm.SetModel, llm.SetModelProgress); - } - #endif /// \endcond public static int GetMaxFreqKHz(int cpuId) From c89ddf4517b20cc6e8d48e569eba6be7ca5e2f31 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 19 Jul 2024 11:37:53 +0300 Subject: [PATCH 014/105] use LLM download --- Editor/LLMEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 39e30dc9..ace849ce 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -35,7 +35,7 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) int newIndex = EditorGUILayout.Popup("Model", llmScript.SelectedModel, options); if (newIndex != llmScript.SelectedModel) { - LLMUnitySetup.DownloadModel(llmScript, newIndex); + llmScript.DownloadDefaultModel(newIndex); } if (GUILayout.Button("Load model", GUILayout.Width(buttonWidth))) From 8e2a1dd6166a9094fa569662967249aa9b0e1eda Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 19 Jul 2024 11:40:36 +0300 Subject: [PATCH 015/105] set template, use short args for download --- Samples~/AndroidDemo/AndroidDemo.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 7424a145..677c1db5 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -39,11 +39,10 @@ IEnumerator Loading() AIText.text = "Downloading model..."; Task downloadTask = llm.DownloadModel( "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true", - null, - SetProgress, - true + SetProgress ); while (!downloadTask.IsCompleted) yield return null; + llm.SetTemplate("alpaca"); DownloadPanel.SetActive(false); ChatPanel.SetActive(true); From fb2685db8981d1b57ab9002a5db92100417780d3 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 20 Jul 2024 15:01:07 +0300 Subject: [PATCH 016/105] check if all bytes are downloaded before starting --- Runtime/ResumingWebClient.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Runtime/ResumingWebClient.cs b/Runtime/ResumingWebClient.cs index cd21f0af..b6e8d4d5 100644 --- a/Runtime/ResumingWebClient.cs +++ b/Runtime/ResumingWebClient.cs @@ -19,6 +19,14 @@ public ResumingWebClient() _context = SynchronizationContext.Current ?? new SynchronizationContext(); } + private long GetRemoteFileSizeAsync(Uri address) + { + WebRequest request = GetWebRequest(address); + request.Method = "HEAD"; + WebResponse response = request.GetResponse(); + return response.ContentLength; + } + public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resume = false, Callback progressCallback = null) { var tcs = new TaskCompletionSource(address); @@ -37,6 +45,14 @@ public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resum WebRequest request = GetWebRequest(address); if (request is HttpWebRequest webRequest && bytesToSkip > 0) { + long remoteFileSize = GetRemoteFileSizeAsync(address); + if (bytesToSkip >= remoteFileSize) + { + LLMUnitySetup.Log($"File is already fully downloaded: {fileName}"); + tcs.TrySetResult(true); + return tcs.Task; + } + filemode = FileMode.Append; LLMUnitySetup.Log($"File exists at {fileName}, skipping {bytesToSkip} bytes"); webRequest.AddRange(bytesToSkip); From b2506b765c9b1550561a3c381bd8417c86a47b0d Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 20 Jul 2024 15:04:50 +0300 Subject: [PATCH 017/105] download on start functionality --- Runtime/LLM.cs | 46 ++++++++++++++++++++++++++++++++++------ Runtime/LLMUnitySetup.cs | 4 +++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index e2e8088e..31ada6c9 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -71,6 +71,11 @@ public class LLM : MonoBehaviour /// the path of the model being used (relative to the Assets/StreamingAssets folder). /// Models with .gguf format are allowed. [Model] public string model = ""; + /// toggle to enable model download on build + [Model] public bool downloadOnBuild = false; + /// the URL of the model to use. + /// Models with .gguf format are allowed. + [ModelDownload] public string modelURL = ""; /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). /// Models with .bin format are allowed. [ModelAdvanced] public string lora = ""; @@ -81,7 +86,8 @@ public class LLM : MonoBehaviour [ModelAdvanced] public int batchSize = 512; /// a base prompt to use as a base for all LLMCharacter objects [TextArea(5, 10), ChatAdvanced] public string basePrompt = ""; - + /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. + public bool modelDownloaded { get; protected set; } = false; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. @@ -101,10 +107,12 @@ public class LLM : MonoBehaviour StreamWrapper logStreamWrapper = null; Thread llmThread = null; List streamWrappers = new List(); + List> progressCallbacks = new List>(); public void SetModelProgress(float progress) { modelProgress = progress; + foreach (Callback progressCallback in progressCallbacks) progressCallback?.Invoke(progress); } /// \endcond @@ -122,22 +130,32 @@ async Task CopyAsset(string path) return path; } + public void ResetSelectedModel() + { + SelectedModel = 0; + modelURL = ""; + model = ""; + } + public async Task DownloadDefaultModel(int optionIndex) { // download default model and disable model editor properties until the model is set + if (optionIndex == 0) + { + ResetSelectedModel(); + return; + } SelectedModel = optionIndex; string modelUrl = LLMUnitySetup.modelOptions[optionIndex].Item2; - if (modelUrl == null) return; + modelURL = modelUrl; string modelName = Path.GetFileName(modelUrl).Split("?")[0]; await DownloadModel(modelUrl, modelName); } - public async Task DownloadModel(string modelUrl, string modelName = null, Callback progressCallback = null, bool overwrite = false) + public async Task DownloadModel(string modelUrl, string modelName, Callback progressCallback = null, bool overwrite = false) { modelProgress = 0; - if (modelName == null) modelName = model; string modelPath = LLMUnitySetup.GetAssetPath(modelName); - Callback callback = (floatArg) => { progressCallback?.Invoke(floatArg); @@ -146,9 +164,21 @@ public async Task DownloadModel(string modelUrl, string modelName = null, Callba await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, SetModel, callback); } - public async Task DownloadModel(string modelUrl, Callback progressCallback = null, bool overwrite = false) + public async Task DownloadModel() + { + await DownloadModel(modelURL, model); + } + + public async Task WaitUntilModelDownloaded(Callback progressCallback = null) + { + if (progressCallback != null) progressCallbacks.Add(progressCallback); + while (!modelDownloaded) await Task.Yield(); + if (progressCallback != null) progressCallbacks.Remove(progressCallback); + } + + public async Task WaitUntilReady() { - await DownloadModel(modelUrl, null, progressCallback, overwrite); + while (!started) await Task.Yield(); } /// @@ -244,6 +274,8 @@ protected virtual string GetLlamaccpArguments() public async void Awake() { if (!enabled) return; + if (downloadOnBuild) await DownloadModel(); + modelDownloaded = true; string arguments = GetLlamaccpArguments(); if (arguments == null) return; if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 5b59e1f2..405173c6 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -43,6 +43,7 @@ public class LocalRemoteAttribute : PropertyAttribute {} public class RemoteAttribute : PropertyAttribute {} public class LocalAttribute : PropertyAttribute {} public class ModelAttribute : PropertyAttribute {} + public class ModelDownloadAttribute : PropertyAttribute {} public class ModelAdvancedAttribute : PropertyAttribute {} public class ChatAttribute : PropertyAttribute {} public class ChatAdvancedAttribute : PropertyAttribute {} @@ -149,7 +150,8 @@ public static void SetDebugMode(DebugModeType newDebugMode) public static string GetAssetPath(string relPath = "") { // Path to store llm server binaries and models - return Path.Combine(Application.streamingAssetsPath, relPath).Replace('\\', '/'); + string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath; + return Path.Combine(assetsDir, relPath).Replace('\\', '/'); } #if UNITY_EDITOR From 0e59840fbab589fef7feaeaacc86074def4c017f Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 20 Jul 2024 15:05:23 +0300 Subject: [PATCH 018/105] add model url if download on build --- Editor/LLMEditor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index ace849ce..cb22d3ff 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -45,7 +45,7 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); if (!string.IsNullOrEmpty(path)) { - llmScript.SelectedModel = 0; + llmScript.ResetSelectedModel(); llmScript.SetModel(path); } }; @@ -90,6 +90,10 @@ public void AddModelSettings(SerializedObject llmScriptSO) { attributeClasses.Add(typeof(ModelAdvancedAttribute)); } + if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) + { + attributeClasses.Add(typeof(ModelDownloadAttribute)); + } ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false); Space(); } From d4de25012b0638fbc39baf902c310cfbfd8a22ad Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 20 Jul 2024 15:06:27 +0300 Subject: [PATCH 019/105] wait on download, server ready --- Samples~/AndroidDemo/AndroidDemo.cs | 34 +++++++++++------------------ Samples~/AndroidDemo/Scene.unity | 12 +++++----- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 677c1db5..bf239f49 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -20,37 +20,29 @@ public class AndroidDemo : MonoBehaviour public Text progressText; int cores; - void Awake() - { - ChatPanel.SetActive(false); - DownloadPanel.SetActive(false); - } - - void Start() + async void Start() { playerText.onSubmit.AddListener(onInputFieldSubmit); playerText.interactable = false; - StartCoroutine(Loading()); + await ShowDownloadScreen(); + await WarmUp(); } - IEnumerator Loading() + async Task ShowDownloadScreen() { + ChatPanel.SetActive(false); DownloadPanel.SetActive(true); - AIText.text = "Downloading model..."; - Task downloadTask = llm.DownloadModel( - "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true", - SetProgress - ); - while (!downloadTask.IsCompleted) yield return null; - llm.SetTemplate("alpaca"); + // await llm.WaitUntilModelDownloaded(SetProgress); DownloadPanel.SetActive(false); - ChatPanel.SetActive(true); - cores = LLMUnitySetup.AndroidGetNumBigCores(); - AIText.text += $"\nWarming up the model...\nWill use {cores} cores"; - Task warmup = llmCharacter.Warmup(); - while (!warmup.IsCompleted) yield return null; + } + async Task WarmUp() + { + llm.SetTemplate("alpaca"); + cores = LLMUnitySetup.AndroidGetNumBigCores(); + AIText.text += $"Warming up the model...\nWill use {cores} cores"; + await llmCharacter.Warmup(); AIText.text = $"Ready when you are ({cores} cores)!"; AIReplyComplete(); } diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity index da2763c6..e1efa5c6 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -1323,22 +1323,24 @@ MonoBehaviour: advancedOptions: 0 remote: 0 port: 13333 - numThreads: -1 + numThreads: 2 numGPULayers: 0 - debug: 1 + debug: 0 parallelPrompts: -1 asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: smol_llama-220m-openhermes.q4_k_m.gguf + model: Phi-3-mini-4k-instruct-q4.gguf + downloadOnBuild: 0 + modelURL: https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true lora: contextSize: 0 batchSize: 512 basePrompt: - SelectedModel: 0 + SelectedModel: 3 modelProgress: 1 modelCopyProgress: 1 modelHide: 1 - chatTemplate: chatml + chatTemplate: phi-3 --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 From 2da395ccac6eb036d78d0f54f25342694ebf746b Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 20 Jul 2024 15:20:07 +0300 Subject: [PATCH 020/105] show progress --- Samples~/AndroidDemo/AndroidDemo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index bf239f49..681f6dfb 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -32,7 +32,7 @@ async Task ShowDownloadScreen() { ChatPanel.SetActive(false); DownloadPanel.SetActive(true); - // await llm.WaitUntilModelDownloaded(SetProgress); + await llm.WaitUntilModelDownloaded(SetProgress); DownloadPanel.SetActive(false); ChatPanel.SetActive(true); } From a2c22fc9773137f2a9c60d58b5accc23f636a148 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sun, 21 Jul 2024 19:46:12 +0300 Subject: [PATCH 021/105] download on build --- Samples~/AndroidDemo/Scene.unity | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity index e1efa5c6..81f310b9 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -1330,7 +1330,7 @@ MonoBehaviour: asynchronousStartup: 1 dontDestroyOnLoad: 1 model: Phi-3-mini-4k-instruct-q4.gguf - downloadOnBuild: 0 + downloadOnBuild: 1 modelURL: https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true lora: contextSize: 0 From b24af15d3457384caa41fe2bc529acf48a3162f7 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sun, 21 Jul 2024 19:46:36 +0300 Subject: [PATCH 022/105] set template only when started --- Runtime/LLM.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 31ada6c9..0635d368 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -218,7 +218,7 @@ public async Task SetLora(string path) public void SetTemplate(string templateName) { chatTemplate = templateName; - llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); + if (started) llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); } /// From 535b7673afad839360d89f96139166b36ec4bca8 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 11:05:42 +0300 Subject: [PATCH 023/105] use symlinks instaed of copying --- Runtime/LLM.cs | 12 +++---- Runtime/LLMCharacter.cs | 4 +-- Runtime/LLMUnitySetup.cs | 71 ++++++++++++++++++++++++---------------- Tests/Runtime/TestLLM.cs | 2 +- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 0635d368..e05c939f 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -117,13 +117,13 @@ public void SetModelProgress(float progress) /// \endcond - async Task CopyAsset(string path) + string CopyAsset(string path) { #if UNITY_EDITOR if (!EditorApplication.isPlaying) { modelCopyProgress = 0; - path = await LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); + path = LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); modelCopyProgress = 1; } #endif @@ -187,10 +187,10 @@ public async Task WaitUntilReady() /// Models supported are in .gguf format. /// /// path to model to use (.gguf format) - public async Task SetModel(string path) + public void SetModel(string path) { // set the model and enable the model editor properties - model = await CopyAsset(path); + model = CopyAsset(path); SetTemplate(ChatTemplate.FromGGUF(LLMUnitySetup.GetAssetPath(model))); #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); @@ -203,9 +203,9 @@ public async Task SetModel(string path) /// Models supported are in .bin format. /// /// path to LORA model to use (.bin format) - public async Task SetLora(string path) + public void SetLora(string path) { - lora = await CopyAsset(path); + lora = CopyAsset(path); #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 8d36ead9..7fb45489 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -319,10 +319,10 @@ public async Task LoadTemplate() /// Set the grammar file of the LLMCharacter /// /// path to the grammar file - public async void SetGrammar(string path) + public void SetGrammar(string path) { #if UNITY_EDITOR - if (!EditorApplication.isPlaying) path = await LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); + if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); #endif grammar = path; InitGrammar(); diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 405173c6..b83865b9 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -7,6 +7,7 @@ using System; using System.IO.Compression; using System.Collections.Generic; +using System.Runtime.InteropServices; /// @defgroup llm LLM /// @defgroup template Chat Templates @@ -182,7 +183,7 @@ public static void CancelDownload(string savePath) public static async Task DownloadFile( string fileUrl, string savePath, bool overwrite = false, - TaskCallback callback = null, Callback progressCallback = null + Callback callback = null, Callback progressCallback = null ) { if (File.Exists(savePath) && !overwrite) @@ -210,13 +211,32 @@ public static async Task DownloadFile( } progressCallback?.Invoke(1f); - if (callback != null) await callback.Invoke(savePath); + callback?.Invoke(savePath); } #if UNITY_EDITOR [HideInInspector] public static float libraryProgress = 1; - public static async Task AddAsset(string assetPath, string basePath) + private static async Task DownloadLibrary() + { + if (libraryProgress < 1) return; + libraryProgress = 0; + string libZip = Path.Combine(Application.temporaryCachePath, Path.GetFileName(LlamaLibURL)); + if (!Directory.Exists(libraryPath)) + { + await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); + ZipFile.ExtractToDirectory(libZip, libraryPath); + File.Delete(libZip); + } + libraryProgress = 1; + } + + private static void SetLibraryProgress(float progress) + { + libraryProgress = progress; + } + + public static string AddAsset(string assetPath, string basePath) { if (!File.Exists(assetPath)) { @@ -231,42 +251,37 @@ public static async Task AddAsset(string assetPath, string basePath) { // if the asset is not in the assets dir copy it over fullPath = Path.Combine(basePathSlash, Path.GetFileName(assetPath)); - Log($"copying {assetPath} to {fullPath}"); AssetDatabase.StartAssetEditing(); - await Task.Run(() => + foreach (string filename in new string[] {fullPath, fullPath + ".meta"}) { - foreach (string filename in new string[] {fullPath, fullPath + ".meta"}) - { - if (File.Exists(fullPath)) - File.Delete(fullPath); - } - File.Copy(assetPath, fullPath); - }); + if (File.Exists(filename)) File.Delete(filename); + } + CreateSymlink(assetPath, fullPath); AssetDatabase.StopAssetEditing(); - Log("copying complete!"); } return fullPath.Substring(basePathSlash.Length + 1); } - private static async Task DownloadLibrary() + public static void CreateSymlink(string sourcePath, string targetPath) { - if (libraryProgress < 1) return; - libraryProgress = 0; - string libZip = Path.Combine(Application.temporaryCachePath, Path.GetFileName(LlamaLibURL)); - if (!Directory.Exists(libraryPath)) - { - await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); - ZipFile.ExtractToDirectory(libZip, libraryPath); - File.Delete(libZip); - } - libraryProgress = 1; - } + bool isDirectory = Directory.Exists(sourcePath); + if (!isDirectory && !File.Exists(sourcePath)) throw new FileNotFoundException($"Source path does not exist: {sourcePath}"); - private static void SetLibraryProgress(float progress) - { - libraryProgress = progress; + bool success; +#if UNITY_STANDALONE_WIN + success = CreateSymbolicLink(targetPath, sourcePath, (int)isDirectory); +#else + success = symlink(sourcePath, targetPath) == 0; +#endif + if (!success) throw new IOException($"Failed to create symbolic link: {targetPath}"); } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); + + [DllImport("libc", SetLastError = true)] + private static extern int symlink(string oldpath, string newpath); + #endif /// \endcond public static int GetMaxFreqKHz(int cpuId) diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index cbb05923..e997a327 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -33,7 +33,7 @@ public async Task Init() string modelPath = "LLMUnityTests/smol_llama-220m-openhermes.q4_k_m.gguf"; string fullModelPath = LLMUnitySetup.GetAssetPath(modelPath); await LLMUnitySetup.DownloadFile(modelUrl, fullModelPath, false, null, null); - await llm.SetModel(fullModelPath); + llm.SetModel(fullModelPath); llm.parallelPrompts = 1; llm.SetTemplate("alpaca"); llm.asynchronousStartup = false; From 2110e2a994baecea33bc9d148e4ccf4d981cadb2 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 20:23:46 +0300 Subject: [PATCH 024/105] move models if using URL --- Editor/LLMBuildProcessor.cs | 100 ++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index a1ebecac..2e17bf84 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -8,17 +8,18 @@ namespace LLMUnity { - public class LLMBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport + public class LLMBuildProcessor : MonoBehaviour, IPreprocessBuildWithReport, IPostprocessBuildWithReport { public int callbackOrder => 0; static string tempDir = Path.Combine(Application.temporaryCachePath, "LLMBuildProcessor", Path.GetFileName(LLMUnitySetup.libraryPath)); - static string foldersMovedCache = Path.Combine(tempDir, "moved.json"); + static List movedPairs = new List(); + static string movedCache = Path.Combine(tempDir, "moved.json"); [InitializeOnLoadMethod] private static void InitializeOnLoad() { if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir); - else ResetLibraryPlatforms(); + else ResetMoves(); } // CALLED BEFORE THE BUILD @@ -26,8 +27,9 @@ public void OnPreprocessBuild(BuildReport report) { // Start listening for errors when build starts Application.logMessageReceived += OnBuildError; - List platforms = GetLibraryPlatformsToHide(report.summary.platform); - HideLibraryPlatforms(platforms); + HideLibraryPlatforms(report.summary.platform); + HideModels(); + if (movedPairs.Count > 0) AssetDatabase.Refresh(); } // CALLED DURING BUILD TO CHECK FOR ERRORS @@ -49,13 +51,40 @@ public void OnPostprocessBuild(BuildReport report) public void BuildCompleted() { Application.logMessageReceived -= OnBuildError; - ResetLibraryPlatforms(); + ResetMoves(); } - static List GetLibraryPlatformsToHide(BuildTarget platform) + static bool MovePath(string source, string target) + { + bool moved = false; + if (File.Exists(source)) + { + File.Move(source, target); + moved = true; + } + else if (Directory.Exists(source)) + { + Directory.Move(source, target); + moved = true; + } + if (moved) + { + movedPairs.Add(new MovedPair {source = source, target = target}); + File.WriteAllText(movedCache, JsonUtility.ToJson(new FoldersMovedWrapper { movedPairs = movedPairs })); + } + return moved; + } + + static void MoveAssetAndMeta(string source, string target) + { + MovePath(source + ".meta", target + ".meta"); + MovePath(source, target); + } + + static void HideLibraryPlatforms(BuildTarget buildPlatform) { List platforms = new List(){ "windows", "macos", "linux", "android" }; - switch (platform) + switch (buildPlatform) { case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: @@ -71,61 +100,44 @@ static List GetLibraryPlatformsToHide(BuildTarget platform) platforms.Remove("android"); break; } - return platforms; - } - static bool MovePath(string source, string target, List foldersMoved = null) - { - bool moved = false; - if (File.Exists(source)) - { - File.Move(source, target); - moved = true; - } - else if (Directory.Exists(source)) - { - Directory.Move(source, target); - moved = true; - } - if (moved && foldersMoved != null) foldersMoved.Add(new FoldersMovedPair {source = source, target = target}); - return moved; - } - - static void HideLibraryPlatforms(List platforms) - { - List foldersMoved = new List(); foreach (string dirname in Directory.GetDirectories(LLMUnitySetup.libraryPath)) { foreach (string platform in platforms) { if (Path.GetFileName(dirname).StartsWith(platform)) { - string movePath = Path.Combine(tempDir, Path.GetFileName(dirname)); - MovePath(dirname + ".meta", movePath + ".meta", foldersMoved); - MovePath(dirname, movePath, foldersMoved); - File.WriteAllText(foldersMovedCache, JsonUtility.ToJson(new FoldersMovedWrapper { foldersMoved = foldersMoved })); + MoveAssetAndMeta(dirname, Path.Combine(tempDir, Path.GetFileName(dirname))); } } } - if (foldersMoved.Count > 0) AssetDatabase.Refresh(); } - static void ResetLibraryPlatforms() + static void HideModels() + { + foreach (LLM llm in FindObjectsOfType()) + { + if (!llm.downloadOnBuild) continue; + if (llm.modelURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.model), Path.Combine(tempDir, Path.GetFileName(llm.model))); + if (llm.loraURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.lora), Path.Combine(tempDir, Path.GetFileName(llm.lora))); + } + } + + static void ResetMoves() { - if (!File.Exists(foldersMovedCache)) return; - List foldersMoved = JsonUtility.FromJson(File.ReadAllText(foldersMovedCache)).foldersMoved; - if (foldersMoved == null) return; + if (!File.Exists(movedCache)) return; + List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).movedPairs; + if (movedPairs == null) return; bool refresh = false; - foreach (var pair in foldersMoved) refresh |= MovePath(pair.target, pair.source); + foreach (var pair in movedPairs) refresh |= MovePath(pair.target, pair.source); if (refresh) AssetDatabase.Refresh(); - - File.Delete(foldersMovedCache); + File.Delete(movedCache); } } [Serializable] - public struct FoldersMovedPair + public struct MovedPair { public string source; public string target; @@ -134,6 +146,6 @@ public struct FoldersMovedPair [Serializable] public class FoldersMovedWrapper { - public List foldersMoved; + public List movedPairs; } } From 00579d50bb36f6dbf4c541524e8593a865f6e6d2 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 20:25:13 +0300 Subject: [PATCH 025/105] add function to extract files from APK --- Runtime/LLMUnitySetup.cs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index b83865b9..3dd76da9 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -8,6 +8,7 @@ using System.IO.Compression; using System.Collections.Generic; using System.Runtime.InteropServices; +using UnityEngine.Networking; /// @defgroup llm LLM /// @defgroup template Chat Templates @@ -214,6 +215,43 @@ public static async Task DownloadFile( callback?.Invoke(savePath); } + public static async Task AndroidExtractFile(string assetName, bool overwrite = false, int chunkSize = 1024*1024) + { + string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName; + string target = GetAssetPath(assetName); + if (!overwrite && File.Exists(target)) + { + Debug.Log($"File {target} already exists"); + return; + } + + Debug.Log($"Extracting {source} to {target}"); + + // UnityWebRequest to read the file from StreamingAssets + UnityWebRequest www = UnityWebRequest.Get(source); + // Send the request and await its completion + var operation = www.SendWebRequest(); + + while (!operation.isDone) await Task.Delay(1); + if (www.result != UnityWebRequest.Result.Success) + { + Debug.LogError("Failed to load file from StreamingAssets: " + www.error); + } + else + { + byte[] buffer = new byte[chunkSize]; + using (Stream responseStream = new MemoryStream(www.downloadHandler.data)) + using (FileStream fileStream = new FileStream(target, FileMode.Create, FileAccess.Write)) + { + int bytesRead; + while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await fileStream.WriteAsync(buffer, 0, bytesRead); + } + } + } + } + #if UNITY_EDITOR [HideInInspector] public static float libraryProgress = 1; From 3ff73a946fde520a00c5bba3df806bff989483a9 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 20:26:06 +0300 Subject: [PATCH 026/105] add lora and model download attributes --- Editor/LLMEditor.cs | 9 ++++----- Editor/PropertyEditor.cs | 17 +++++++++++++++-- Runtime/LLMUnitySetup.cs | 3 ++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index cb22d3ff..cb778f9c 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -86,15 +86,14 @@ public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bo public void AddModelSettings(SerializedObject llmScriptSO) { List attributeClasses = new List { typeof(ModelAttribute) }; + List excludeAttributeClasses = new List { typeof(ModelDownloadAttribute), typeof(ModelDownloadAdvancedAttribute) }; + if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) excludeAttributeClasses.Remove(typeof(ModelDownloadAttribute)); if (llmScriptSO.FindProperty("advancedOptions").boolValue) { attributeClasses.Add(typeof(ModelAdvancedAttribute)); + if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) excludeAttributeClasses.Remove(typeof(ModelDownloadAdvancedAttribute)); } - if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) - { - attributeClasses.Add(typeof(ModelDownloadAttribute)); - } - ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false); + ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false, excludeAttributeClasses); Space(); } diff --git a/Editor/PropertyEditor.cs b/Editor/PropertyEditor.cs index 7229804a..e5175a87 100644 --- a/Editor/PropertyEditor.cs +++ b/Editor/PropertyEditor.cs @@ -94,10 +94,23 @@ public List GetPropertiesOfClass(SerializedObject so, List attributeClasses, bool addSpace = true) + public void ShowPropertiesOfClass(string title, SerializedObject so, List attributeClasses, bool addSpace = true, List excludeAttributeClasses = null) { // display a property if it belongs to a certain class and/or has a specific attribute class List properties = GetPropertiesOfClass(so, attributeClasses); + if (excludeAttributeClasses != null) + { + List excludeProperties = GetPropertiesOfClass(so, excludeAttributeClasses); + List removeProperties = new List(); + foreach (SerializedProperty excprop in excludeProperties) + { + foreach (SerializedProperty prop in properties) + { + if (prop.displayName == excprop.displayName) removeProperties.Add(prop); + } + } + foreach (SerializedProperty prop in removeProperties) properties.Remove(prop); + } if (properties.Count == 0) return; if (title != "") EditorGUILayout.LabelField(title, EditorStyles.boldLabel); foreach (SerializedProperty prop in properties) @@ -141,7 +154,7 @@ public Attribute GetPropertyAttribute(SerializedProperty prop, Type attributeCla { foreach (Attribute attr in fieldInfo.GetCustomAttributes(attributeClass, true)) { - if (attr.GetType() == attributeClass) + if (attributeClass.IsAssignableFrom(attr.GetType())) return attr; } } diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 3dd76da9..bc9d9fb1 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -45,7 +45,8 @@ public class LocalRemoteAttribute : PropertyAttribute {} public class RemoteAttribute : PropertyAttribute {} public class LocalAttribute : PropertyAttribute {} public class ModelAttribute : PropertyAttribute {} - public class ModelDownloadAttribute : PropertyAttribute {} + public class ModelDownloadAttribute : ModelAttribute {} + public class ModelDownloadAdvancedAttribute : ModelAdvancedAttribute {} public class ModelAdvancedAttribute : PropertyAttribute {} public class ChatAttribute : PropertyAttribute {} public class ChatAdvancedAttribute : PropertyAttribute {} From 7eec43e0d3b8f356c671f3e2b99ffc833e755bf3 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 20:26:44 +0300 Subject: [PATCH 027/105] download or extract model and lora --- Runtime/LLM.cs | 63 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index e05c939f..43ac2799 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -68,17 +68,20 @@ public class LLM : MonoBehaviour [LLMAdvanced] public bool asynchronousStartup = true; /// select to not destroy the LLM GameObject when loading a new Scene. [LLMAdvanced] public bool dontDestroyOnLoad = true; + /// toggle to enable model download on build + [Model] public bool downloadOnBuild = false; /// the path of the model being used (relative to the Assets/StreamingAssets folder). /// Models with .gguf format are allowed. [Model] public string model = ""; - /// toggle to enable model download on build - [Model] public bool downloadOnBuild = false; /// the URL of the model to use. /// Models with .gguf format are allowed. [ModelDownload] public string modelURL = ""; /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). /// Models with .bin format are allowed. [ModelAdvanced] public string lora = ""; + /// the URL of the LORA to use. + /// Models with .bin format are allowed. + [ModelDownloadAdvanced] public string loraURL = ""; /// Size of the prompt context (0 = context size of the model). /// This is the number of tokens the model can take as input when generating responses. [ModelAdvanced] public int contextSize = 0; @@ -87,7 +90,7 @@ public class LLM : MonoBehaviour /// a base prompt to use as a base for all LLMCharacter objects [TextArea(5, 10), ChatAdvanced] public string basePrompt = ""; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. - public bool modelDownloaded { get; protected set; } = false; + public bool modelsDownloaded { get; protected set; } = false; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. @@ -96,6 +99,7 @@ public class LLM : MonoBehaviour /// \cond HIDE public int SelectedModel = 0; [HideInInspector] public float modelProgress = 1; + [HideInInspector] public float loraProgress = 1; [HideInInspector] public float modelCopyProgress = 1; [HideInInspector] public bool modelHide = true; @@ -107,12 +111,19 @@ public class LLM : MonoBehaviour StreamWrapper logStreamWrapper = null; Thread llmThread = null; List streamWrappers = new List(); - List> progressCallbacks = new List>(); + List> modelProgressCallbacks = new List>(); + List> loraProgressCallbacks = new List>(); public void SetModelProgress(float progress) { modelProgress = progress; - foreach (Callback progressCallback in progressCallbacks) progressCallback?.Invoke(progress); + foreach (Callback modelProgressCallback in modelProgressCallbacks) modelProgressCallback?.Invoke(progress); + } + + public void SetLoraProgress(float progress) + { + loraProgress = progress; + foreach (Callback loraProgressCallback in loraProgressCallbacks) loraProgressCallback?.Invoke(progress); } /// \endcond @@ -152,28 +163,39 @@ public async Task DownloadDefaultModel(int optionIndex) await DownloadModel(modelUrl, modelName); } - public async Task DownloadModel(string modelUrl, string modelName, Callback progressCallback = null, bool overwrite = false) + public async Task DownloadModel(string modelUrl, string modelName, bool overwrite = false) { modelProgress = 0; string modelPath = LLMUnitySetup.GetAssetPath(modelName); - Callback callback = (floatArg) => - { - progressCallback?.Invoke(floatArg); - SetModelProgress(floatArg); - }; - await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, SetModel, callback); + await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, SetModel, SetModelProgress); + } + + public async Task DownloadLora(string loraUrl, string loraName, bool overwrite = false) + { + loraProgress = 0; + string loraPath = LLMUnitySetup.GetAssetPath(loraName); + await LLMUnitySetup.DownloadFile(loraUrl, loraPath, overwrite, SetLora, SetLoraProgress); + } + + public async Task DownloadModels() + { + if (modelURL != "") await DownloadModel(modelURL, model); + if (loraURL != "") await DownloadLora(loraURL, lora); } - public async Task DownloadModel() + public async Task AndroidExtractModels() { - await DownloadModel(modelURL, model); + if (!downloadOnBuild || modelURL == "") await LLMUnitySetup.AndroidExtractFile(model); + if (!downloadOnBuild || loraURL == "") await LLMUnitySetup.AndroidExtractFile(lora); } - public async Task WaitUntilModelDownloaded(Callback progressCallback = null) + public async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) { - if (progressCallback != null) progressCallbacks.Add(progressCallback); - while (!modelDownloaded) await Task.Yield(); - if (progressCallback != null) progressCallbacks.Remove(progressCallback); + if (modelProgressCallback != null) modelProgressCallbacks.Add(modelProgressCallback); + if (loraProgressCallback != null) loraProgressCallbacks.Add(loraProgressCallback); + while (!modelsDownloaded) await Task.Yield(); + if (modelProgressCallback != null) modelProgressCallbacks.Remove(modelProgressCallback); + if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); } public async Task WaitUntilReady() @@ -274,8 +296,9 @@ protected virtual string GetLlamaccpArguments() public async void Awake() { if (!enabled) return; - if (downloadOnBuild) await DownloadModel(); - modelDownloaded = true; + if (downloadOnBuild) await DownloadModels(); + modelsDownloaded = true; + if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); string arguments = GetLlamaccpArguments(); if (arguments == null) return; if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); From 8a65cd7501792bb940a455ce518da9ab3c983055 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Mon, 22 Jul 2024 20:52:13 +0300 Subject: [PATCH 028/105] do not change template when downloading models at start --- Runtime/LLM.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 43ac2799..76e31ab4 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -163,11 +163,11 @@ public async Task DownloadDefaultModel(int optionIndex) await DownloadModel(modelUrl, modelName); } - public async Task DownloadModel(string modelUrl, string modelName, bool overwrite = false) + public async Task DownloadModel(string modelUrl, string modelName, bool overwrite = false, bool setTemplate = true) { modelProgress = 0; string modelPath = LLMUnitySetup.GetAssetPath(modelName); - await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, SetModel, SetModelProgress); + await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, (string path) => SetModel(path, setTemplate), SetModelProgress); } public async Task DownloadLora(string loraUrl, string loraName, bool overwrite = false) @@ -177,10 +177,10 @@ public async Task DownloadLora(string loraUrl, string loraName, bool overwrite = await LLMUnitySetup.DownloadFile(loraUrl, loraPath, overwrite, SetLora, SetLoraProgress); } - public async Task DownloadModels() + public async Task DownloadModels(bool overwrite = false) { - if (modelURL != "") await DownloadModel(modelURL, model); - if (loraURL != "") await DownloadLora(loraURL, lora); + if (modelURL != "") await DownloadModel(modelURL, model, overwrite, false); + if (loraURL != "") await DownloadLora(loraURL, lora, overwrite); } public async Task AndroidExtractModels() @@ -209,11 +209,11 @@ public async Task WaitUntilReady() /// Models supported are in .gguf format. /// /// path to model to use (.gguf format) - public void SetModel(string path) + public void SetModel(string path, bool setTemplate = true) { // set the model and enable the model editor properties model = CopyAsset(path); - SetTemplate(ChatTemplate.FromGGUF(LLMUnitySetup.GetAssetPath(model))); + if (setTemplate) SetTemplate(ChatTemplate.FromGGUF(LLMUnitySetup.GetAssetPath(model))); #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif From cf0bd2fa47d2cd48ba1a27124ff866728d6e08eb Mon Sep 17 00:00:00 2001 From: amakropoulos Date: Tue, 23 Jul 2024 07:11:28 +0000 Subject: [PATCH 029/105] update changelogs --- CHANGELOG.md | 6 ++++++ CHANGELOG.release.md | 13 +------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d45dc1d2..7895c1ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.1.0 +#### 🚀 Features + +- Android deployment (PR: #194) + + ## v2.0.3 #### 🚀 Features diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index f692fe7f..650d0934 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -1,15 +1,4 @@ ### 🚀 Features -- Add LLM selector in Inspector mode (PR: #182) -- Allow to save chat history at custom path (PR: #179) -- Use asynchronous startup by default (PR: #186) -- Assign LLM if not set according to the scene and hierarchy (PR: #187) -- Allow to set log level (PR: #189) -- Allow to add callback functions for error messages (PR: #190) -- Allow to set a LLM base prompt for all LLMCharacter objects (PR: #192) - -### 🐛 Fixes - -- set higher priority for mac build with Accelerate than without (PR: #180) -- Fix duplicate bos warning +- Android deployment (PR: #194) From 0b5264e19cbb33112bd352b5d833e10e61fd9864 Mon Sep 17 00:00:00 2001 From: amakropoulos Date: Fri, 26 Jul 2024 06:51:47 +0000 Subject: [PATCH 030/105] update VERSION --- .github/doxygen/Doxyfile | 2 +- Runtime/LLMUnitySetup.cs | 2 +- VERSION | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/doxygen/Doxyfile b/.github/doxygen/Doxyfile index f0f3fa3a..fc99709d 100644 --- a/.github/doxygen/Doxyfile +++ b/.github/doxygen/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "LLM for Unity" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = v2.0.3 +PROJECT_NUMBER = v2.1.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index bc9d9fb1..a7549de0 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -71,7 +71,7 @@ public class LLMUnitySetup { // DON'T CHANGE! the version is autocompleted with a GitHub action /// LLM for Unity version - public static string Version = "v2.0.3"; + public static string Version = "v2.1.0"; /// LlamaLib version public static string LlamaLibVersion = "v1.1.5"; /// LlamaLib url diff --git a/VERSION b/VERSION index f256be60..1defe531 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.3 +v2.1.0 diff --git a/package.json b/package.json index bb83f63a..61a9f4ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai.undream.llm", - "version": "2.0.3", + "version": "2.1.0", "displayName": "LLM for Unity", "description": "LLM for Unity allows to run and distribute Large Language Models (LLMs) in the Unity engine.", "unity": "2022.3", From f0f49570d534180b00e703acb8527f95ed323ffd Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 23 Jul 2024 15:58:21 +0300 Subject: [PATCH 031/105] button width static --- Editor/PropertyEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/PropertyEditor.cs b/Editor/PropertyEditor.cs index e5175a87..87a40938 100644 --- a/Editor/PropertyEditor.cs +++ b/Editor/PropertyEditor.cs @@ -8,7 +8,7 @@ namespace LLMUnity { public class PropertyEditor : Editor { - protected int buttonWidth = 150; + public static int buttonWidth = 150; public void AddScript(SerializedObject llmScriptSO) { From fff371250cc8567531b6bfcc17a66215b15d8c06 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 23 Jul 2024 15:59:01 +0300 Subject: [PATCH 032/105] LLM manager --- Editor/LLMManagerEditor.cs | 186 ++++++++++++++++++++++++++++++++ Editor/LLMManagerEditor.cs.meta | 11 ++ Runtime/LLMManager.cs | 64 +++++++++++ Runtime/LLMManager.cs.meta | 11 ++ Runtime/LLMUnitySetup.cs | 5 + 5 files changed, 277 insertions(+) create mode 100644 Editor/LLMManagerEditor.cs create mode 100644 Editor/LLMManagerEditor.cs.meta create mode 100644 Runtime/LLMManager.cs create mode 100644 Runtime/LLMManager.cs.meta diff --git a/Editor/LLMManagerEditor.cs b/Editor/LLMManagerEditor.cs new file mode 100644 index 00000000..d4adaa18 --- /dev/null +++ b/Editor/LLMManagerEditor.cs @@ -0,0 +1,186 @@ +using UnityEditor; +using UnityEngine; +using UnityEditorInternal; +using System; +using System.Collections.Generic; + +namespace LLMUnity +{ + [CustomEditor(typeof(LLMManager))] + public class LLMManagerEditor : Editor + { + private ReorderableList modelList; + static float nameColumnWidth = 250f; + static float textColumnWidth = 150f; + static float includeInBuildColumnWidth = 50f; + static float actionColumnWidth = 30f; + static int elementPadding = 10; + static GUIContent trashIcon; + static List modelOptions; + static List modelURLs; + + static void ResetModelOptions() + { + List existingOptions = new List(); + foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); + modelOptions = new List(); + modelURLs = new List(); + for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) + { + string url = LLMUnitySetup.modelOptions[i].Item2; + if (existingOptions.Contains(url)) continue; + modelOptions.Add(LLMUnitySetup.modelOptions[i].Item1); + modelURLs.Add(url); + } + } + + List getColumnPositions(float offsetX) + { + List offsets = new List(); + float[] widths = new float[] {actionColumnWidth, nameColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth}; + float offset = offsetX; + foreach (float width in widths) + { + offsets.Add(offset); + offset += width + elementPadding; + } + return new List(){offsets.ToArray(), widths}; + } + + void UpdateModels(bool resetOptions = false) + { + LLMManager.Save(); + if (resetOptions) ResetModelOptions(); + Repaint(); + } + + void OnEnable() + { + ResetModelOptions(); + trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); + + modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), true, true, true, true) + { + drawElementCallback = async(rect, index, isActive, isFocused) => + { + if (index >= LLMManager.modelEntries.Count) return; + + List positions = getColumnPositions(rect.x); + float[] offsets = positions[0]; + float[] widths = positions[1]; + var actionRect = new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight); + var nameRect = new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight); + var urlRect = new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight); + var pathRect = new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight); + var includeInBuildRect = new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight); + var entry = LLMManager.modelEntries[index]; + + bool hasPath = entry.localPath != null && entry.localPath != ""; + bool hasURL = entry.url != null && entry.url != ""; + + + if (GUI.Button(actionRect, trashIcon)) + { + LLMManager.modelEntries.Remove(entry); + UpdateModels(true); + } + + DrawCopyableLabel(nameRect, entry.name); + + if (hasURL) + { + DrawCopyableLabel(urlRect, entry.url); + } + else if (hasPath) + { + string newURL = EditorGUI.TextField(urlRect, entry.url); + if (newURL != entry.url) + { + entry.url = newURL; + UpdateModels(); + } + } + else + { + urlRect.width = PropertyEditor.buttonWidth; + int newIndex = EditorGUI.Popup(urlRect, 0, modelOptions.ToArray()); + if (newIndex != 0) + { + await LLMManager.DownloadModel(entry, modelURLs[newIndex], modelOptions[newIndex]); + UpdateModels(true); + } + } + + if (hasPath) + { + DrawCopyableLabel(pathRect, entry.localPath); + } + else + { + pathRect.width = PropertyEditor.buttonWidth; + if (GUI.Button(pathRect, "Load model")) + { + EditorApplication.delayCall += () => + { + string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); + if (!string.IsNullOrEmpty(path)) + { + entry.localPath = path; + entry.name = LLMManager.ModelPathToName(path); + UpdateModels(); + } + }; + } + } + + bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); + if (includeInBuild != entry.includeInBuild) + { + entry.includeInBuild = includeInBuild; + UpdateModels(); + } + }, + drawHeaderCallback = (rect) => + { + List positions = getColumnPositions(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1); + float[] offsets = positions[0]; + float[] widths = positions[1]; + EditorGUI.LabelField(new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight), ""); + EditorGUI.LabelField(new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight), "Model"); + EditorGUI.LabelField(new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight), "URL"); + EditorGUI.LabelField(new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight), "Local Path"); + EditorGUI.LabelField(new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight), "Build"); + } + }; + } + + private void DrawCopyableLabel(Rect rect, string text) + { + EditorGUI.LabelField(rect, text); + if (Event.current.type == EventType.ContextClick && rect.Contains(Event.current.mousePosition)) + { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Copy"), false, () => CopyToClipboard(text)); + menu.ShowAsContext(); + Event.current.Use(); + } + } + + private void CopyToClipboard(string text) + { + TextEditor te = new TextEditor + { + text = text + }; + te.SelectAll(); + te.Copy(); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + modelList.DoLayoutList(); + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/LLMManagerEditor.cs.meta b/Editor/LLMManagerEditor.cs.meta new file mode 100644 index 00000000..8e49e889 --- /dev/null +++ b/Editor/LLMManagerEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4209594efd29689d490881e0f61b9270 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs new file mode 100644 index 00000000..0d8d3539 --- /dev/null +++ b/Runtime/LLMManager.cs @@ -0,0 +1,64 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace LLMUnity +{ + [Serializable] + public class ModelEntry + { + public string name; + public string url; + public string localPath; + public bool includeInBuild; + } + + [Serializable] + public class ModelEntryList + { + public List modelEntries; + } + + public class LLMManager : MonoBehaviour + { + public static List modelEntries = new List(); + + [InitializeOnLoadMethod] + static void InitializeOnLoad() + { + Load(); + } + + public static string ModelPathToName(string path) + { + return Path.GetFileNameWithoutExtension(path.Split("?")[0]); + } + + public static async Task DownloadModel(ModelEntry entry, string url, string name = null) + { + string modelName = Path.GetFileName(url).Split("?")[0]; + string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); + await LLMUnitySetup.DownloadFile(url, modelPath); + entry.name = name == null ? ModelPathToName(url) : name; + entry.url = url; + entry.localPath = modelPath; + } + + public static void Save() + { + Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); + File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new ModelEntryList { modelEntries = modelEntries })); + } + + public static void Load() + { + if (!File.Exists(LLMUnitySetup.modelListPath)) return; + modelEntries = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)).modelEntries; + } + } +} +#endif diff --git a/Runtime/LLMManager.cs.meta b/Runtime/LLMManager.cs.meta new file mode 100644 index 00000000..ea565c07 --- /dev/null +++ b/Runtime/LLMManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 936a5c66e859e31489f7ab1b78acb987 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index a7549de0..a5184093 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -78,6 +78,10 @@ public class LLMUnitySetup public static string LlamaLibURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}/undreamai-{LlamaLibVersion}-llamacpp.zip"; /// LlamaLib path public static string libraryPath = GetAssetPath(Path.GetFileName(LlamaLibURL).Replace(".zip", "")); + /// Model download path + public static string modelDownloadPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); + /// Model list for project + public static string modelListPath = Path.Combine(Application.temporaryCachePath, "modelCache.json"); /// Default models for download [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] @@ -86,6 +90,7 @@ public class LLMUnitySetup ("Mistral 7B Instruct v0.2 (medium, best overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), ("OpenHermes 2.5 7B (medium, best for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), ("Phi 3 (small, great)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), + ("Test", "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"), }; /// Add callback function to call for error logs From 2c93c34974a7c76e1c58b283eea9b41ae2a7e253 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 23 Jul 2024 17:12:38 +0300 Subject: [PATCH 033/105] move LLMManager Editor inside LLM --- Editor/LLMEditor.cs | 198 +++++++++++++++++++++++++++----- Editor/LLMManagerEditor.cs | 186 ------------------------------ Editor/LLMManagerEditor.cs.meta | 11 -- Runtime/LLM.cs | 1 + Runtime/LLMManager.cs | 2 +- 5 files changed, 171 insertions(+), 227 deletions(-) delete mode 100644 Editor/LLMManagerEditor.cs delete mode 100644 Editor/LLMManagerEditor.cs.meta diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index cb778f9c..7c5c0920 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; +using UnityEditorInternal; using UnityEngine; namespace LLMUnity @@ -9,6 +10,16 @@ namespace LLMUnity [CustomEditor(typeof(LLM))] public class LLMEditor : PropertyEditor { + private ReorderableList modelList; + static float nameColumnWidth = 250f; + static float textColumnWidth = 150f; + static float includeInBuildColumnWidth = 50f; + static float actionColumnWidth = 30f; + static int elementPadding = 10; + static GUIContent trashIcon; + static List modelOptions; + static List modelURLs; + protected override Type[] GetPropertyTypes() { return new Type[] { typeof(LLM) }; @@ -24,37 +35,10 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) { - EditorGUILayout.BeginHorizontal(); - - string[] options = new string[LLMUnitySetup.modelOptions.Length]; - for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) - { - options[i] = LLMUnitySetup.modelOptions[i].Item1; - } - - int newIndex = EditorGUILayout.Popup("Model", llmScript.SelectedModel, options); - if (newIndex != llmScript.SelectedModel) - { - llmScript.DownloadDefaultModel(newIndex); - } - - if (GUILayout.Button("Load model", GUILayout.Width(buttonWidth))) - { - EditorApplication.delayCall += () => - { - string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); - if (!string.IsNullOrEmpty(path)) - { - llmScript.ResetSelectedModel(); - llmScript.SetModel(path); - } - }; - } - EditorGUILayout.EndHorizontal(); - + modelList.DoLayoutList(); string[] templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); int index = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), llmScript.chatTemplate); - newIndex = EditorGUILayout.Popup("Chat Template", index, templateOptions); + int newIndex = EditorGUILayout.Popup("Chat Template", index, templateOptions); if (newIndex != index) { llmScript.SetTemplate(ChatTemplate.templatesDescription[templateOptions[newIndex]]); @@ -102,6 +86,162 @@ void ShowProgress(float progress, string progressText) if (progress != 1) EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progress, progressText); } + static void ResetModelOptions() + { + List existingOptions = new List(); + foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); + modelOptions = new List(); + modelURLs = new List(); + for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) + { + string url = LLMUnitySetup.modelOptions[i].Item2; + if (existingOptions.Contains(url)) continue; + modelOptions.Add(LLMUnitySetup.modelOptions[i].Item1); + modelURLs.Add(url); + } + } + + List getColumnPositions(float offsetX) + { + List offsets = new List(); + float[] widths = new float[] {actionColumnWidth, nameColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth}; + float offset = offsetX; + foreach (float width in widths) + { + offsets.Add(offset); + offset += width + elementPadding; + } + return new List(){offsets.ToArray(), widths}; + } + + void UpdateModels(bool resetOptions = false) + { + LLMManager.Save(); + if (resetOptions) ResetModelOptions(); + Repaint(); + } + + void OnEnable() + { + ResetModelOptions(); + trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); + + modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), true, true, true, true) + { + drawElementCallback = async(rect, index, isActive, isFocused) => + { + if (index >= LLMManager.modelEntries.Count) return; + + List positions = getColumnPositions(rect.x); + float[] offsets = positions[0]; + float[] widths = positions[1]; + var actionRect = new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight); + var nameRect = new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight); + var urlRect = new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight); + var pathRect = new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight); + var includeInBuildRect = new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight); + var entry = LLMManager.modelEntries[index]; + + bool hasPath = entry.localPath != null && entry.localPath != ""; + bool hasURL = entry.url != null && entry.url != ""; + + if (GUI.Button(actionRect, trashIcon)) + { + LLMManager.modelEntries.Remove(entry); + UpdateModels(true); + } + + DrawCopyableLabel(nameRect, entry.name); + + if (hasURL) + { + DrawCopyableLabel(urlRect, entry.url); + } + else if (hasPath) + { + string newURL = EditorGUI.TextField(urlRect, entry.url); + if (newURL != entry.url) + { + entry.url = newURL; + UpdateModels(); + } + } + else + { + urlRect.width = buttonWidth; + int newIndex = EditorGUI.Popup(urlRect, 0, modelOptions.ToArray()); + if (newIndex != 0) + { + await LLMManager.DownloadModel(entry, modelURLs[newIndex], modelOptions[newIndex]); + UpdateModels(true); + } + } + + if (hasPath) + { + DrawCopyableLabel(pathRect, entry.localPath); + } + else + { + pathRect.width = buttonWidth; + if (GUI.Button(pathRect, "Load model")) + { + EditorApplication.delayCall += () => + { + string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); + if (!string.IsNullOrEmpty(path)) + { + entry.localPath = path; + entry.name = LLMManager.ModelPathToName(path); + UpdateModels(); + } + }; + } + } + + bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); + if (includeInBuild != entry.includeInBuild) + { + entry.includeInBuild = includeInBuild; + UpdateModels(); + } + }, + drawHeaderCallback = (rect) => + { + List positions = getColumnPositions(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1); + float[] offsets = positions[0]; + float[] widths = positions[1]; + EditorGUI.LabelField(new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight), ""); + EditorGUI.LabelField(new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight), "Model"); + EditorGUI.LabelField(new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight), "URL"); + EditorGUI.LabelField(new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight), "Local Path"); + EditorGUI.LabelField(new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight), "Build"); + } + }; + } + + private void DrawCopyableLabel(Rect rect, string text) + { + EditorGUI.LabelField(rect, text); + if (Event.current.type == EventType.ContextClick && rect.Contains(Event.current.mousePosition)) + { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Copy"), false, () => CopyToClipboard(text)); + menu.ShowAsContext(); + Event.current.Use(); + } + } + + private void CopyToClipboard(string text) + { + TextEditor te = new TextEditor + { + text = text + }; + te.SelectAll(); + te.Copy(); + } + public override void OnInspectorGUI() { LLM llmScript = (LLM)target; diff --git a/Editor/LLMManagerEditor.cs b/Editor/LLMManagerEditor.cs deleted file mode 100644 index d4adaa18..00000000 --- a/Editor/LLMManagerEditor.cs +++ /dev/null @@ -1,186 +0,0 @@ -using UnityEditor; -using UnityEngine; -using UnityEditorInternal; -using System; -using System.Collections.Generic; - -namespace LLMUnity -{ - [CustomEditor(typeof(LLMManager))] - public class LLMManagerEditor : Editor - { - private ReorderableList modelList; - static float nameColumnWidth = 250f; - static float textColumnWidth = 150f; - static float includeInBuildColumnWidth = 50f; - static float actionColumnWidth = 30f; - static int elementPadding = 10; - static GUIContent trashIcon; - static List modelOptions; - static List modelURLs; - - static void ResetModelOptions() - { - List existingOptions = new List(); - foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); - modelOptions = new List(); - modelURLs = new List(); - for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) - { - string url = LLMUnitySetup.modelOptions[i].Item2; - if (existingOptions.Contains(url)) continue; - modelOptions.Add(LLMUnitySetup.modelOptions[i].Item1); - modelURLs.Add(url); - } - } - - List getColumnPositions(float offsetX) - { - List offsets = new List(); - float[] widths = new float[] {actionColumnWidth, nameColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth}; - float offset = offsetX; - foreach (float width in widths) - { - offsets.Add(offset); - offset += width + elementPadding; - } - return new List(){offsets.ToArray(), widths}; - } - - void UpdateModels(bool resetOptions = false) - { - LLMManager.Save(); - if (resetOptions) ResetModelOptions(); - Repaint(); - } - - void OnEnable() - { - ResetModelOptions(); - trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); - - modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), true, true, true, true) - { - drawElementCallback = async(rect, index, isActive, isFocused) => - { - if (index >= LLMManager.modelEntries.Count) return; - - List positions = getColumnPositions(rect.x); - float[] offsets = positions[0]; - float[] widths = positions[1]; - var actionRect = new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight); - var nameRect = new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight); - var urlRect = new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight); - var pathRect = new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight); - var includeInBuildRect = new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight); - var entry = LLMManager.modelEntries[index]; - - bool hasPath = entry.localPath != null && entry.localPath != ""; - bool hasURL = entry.url != null && entry.url != ""; - - - if (GUI.Button(actionRect, trashIcon)) - { - LLMManager.modelEntries.Remove(entry); - UpdateModels(true); - } - - DrawCopyableLabel(nameRect, entry.name); - - if (hasURL) - { - DrawCopyableLabel(urlRect, entry.url); - } - else if (hasPath) - { - string newURL = EditorGUI.TextField(urlRect, entry.url); - if (newURL != entry.url) - { - entry.url = newURL; - UpdateModels(); - } - } - else - { - urlRect.width = PropertyEditor.buttonWidth; - int newIndex = EditorGUI.Popup(urlRect, 0, modelOptions.ToArray()); - if (newIndex != 0) - { - await LLMManager.DownloadModel(entry, modelURLs[newIndex], modelOptions[newIndex]); - UpdateModels(true); - } - } - - if (hasPath) - { - DrawCopyableLabel(pathRect, entry.localPath); - } - else - { - pathRect.width = PropertyEditor.buttonWidth; - if (GUI.Button(pathRect, "Load model")) - { - EditorApplication.delayCall += () => - { - string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); - if (!string.IsNullOrEmpty(path)) - { - entry.localPath = path; - entry.name = LLMManager.ModelPathToName(path); - UpdateModels(); - } - }; - } - } - - bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); - if (includeInBuild != entry.includeInBuild) - { - entry.includeInBuild = includeInBuild; - UpdateModels(); - } - }, - drawHeaderCallback = (rect) => - { - List positions = getColumnPositions(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1); - float[] offsets = positions[0]; - float[] widths = positions[1]; - EditorGUI.LabelField(new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight), ""); - EditorGUI.LabelField(new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight), "Model"); - EditorGUI.LabelField(new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight), "URL"); - EditorGUI.LabelField(new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight), "Local Path"); - EditorGUI.LabelField(new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight), "Build"); - } - }; - } - - private void DrawCopyableLabel(Rect rect, string text) - { - EditorGUI.LabelField(rect, text); - if (Event.current.type == EventType.ContextClick && rect.Contains(Event.current.mousePosition)) - { - GenericMenu menu = new GenericMenu(); - menu.AddItem(new GUIContent("Copy"), false, () => CopyToClipboard(text)); - menu.ShowAsContext(); - Event.current.Use(); - } - } - - private void CopyToClipboard(string text) - { - TextEditor te = new TextEditor - { - text = text - }; - te.SelectAll(); - te.Copy(); - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - modelList.DoLayoutList(); - serializedObject.ApplyModifiedProperties(); - } - } -} diff --git a/Editor/LLMManagerEditor.cs.meta b/Editor/LLMManagerEditor.cs.meta deleted file mode 100644 index 8e49e889..00000000 --- a/Editor/LLMManagerEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4209594efd29689d490881e0f61b9270 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 76e31ab4..1811b942 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -97,6 +97,7 @@ public class LLM : MonoBehaviour public bool failed { get; protected set; } = false; /// \cond HIDE + public LLMManager llmManager = new LLMManager(); public int SelectedModel = 0; [HideInInspector] public float modelProgress = 1; [HideInInspector] public float loraProgress = 1; diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 0d8d3539..2496a8b6 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -23,7 +23,7 @@ public class ModelEntryList public List modelEntries; } - public class LLMManager : MonoBehaviour + public class LLMManager { public static List modelEntries = new List(); From f9e24d467bc016b114d62556d6ff6ba4680751b3 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 23 Jul 2024 20:05:17 +0300 Subject: [PATCH 034/105] add trash icon --- Resources.meta | 8 ++ Resources/llmunity_trash_icon.png | Bin 0 -> 3334 bytes Resources/llmunity_trash_icon.png.meta | 140 +++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 Resources.meta create mode 100644 Resources/llmunity_trash_icon.png create mode 100644 Resources/llmunity_trash_icon.png.meta diff --git a/Resources.meta b/Resources.meta new file mode 100644 index 00000000..cd69ccb5 --- /dev/null +++ b/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 688bae55bf18bd75dbc7fee333923c15 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/llmunity_trash_icon.png b/Resources/llmunity_trash_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7457cc94936e3f63e821d5fdea61645c7af4e978 GIT binary patch literal 3334 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 G3I8V^H`opU literal 0 HcmV?d00001 diff --git a/Resources/llmunity_trash_icon.png.meta b/Resources/llmunity_trash_icon.png.meta new file mode 100644 index 00000000..9b334b18 --- /dev/null +++ b/Resources/llmunity_trash_icon.png.meta @@ -0,0 +1,140 @@ +fileFormatVersion: 2 +guid: 0e04eced3ed2d120e84e7c10c8b32ddc +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: From 488063682b14b579cb69e7dc218b5055e7f76812 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 23 Jul 2024 20:05:58 +0300 Subject: [PATCH 035/105] migrate LLMManager Editor to LLM Editor --- Editor/LLMBuildProcessor.cs | 4 +- Editor/LLMEditor.cs | 169 ++++++++++++++++++++---------------- Runtime/LLM.cs | 118 +++---------------------- Runtime/LLMManager.cs | 74 ++++++++++++++-- Runtime/LLMUnitySetup.cs | 7 ++ 5 files changed, 181 insertions(+), 191 deletions(-) diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index 2e17bf84..05cc6238 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -117,8 +117,8 @@ static void HideModels() { foreach (LLM llm in FindObjectsOfType()) { - if (!llm.downloadOnBuild) continue; - if (llm.modelURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.model), Path.Combine(tempDir, Path.GetFileName(llm.model))); + // if (!llm.downloadOnBuild) continue; + // if (llm.modelURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.model), Path.Combine(tempDir, Path.GetFileName(llm.model))); if (llm.loraURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.lora), Path.Combine(tempDir, Path.GetFileName(llm.lora))); } } diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 7c5c0920..63f95455 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -11,14 +11,16 @@ namespace LLMUnity public class LLMEditor : PropertyEditor { private ReorderableList modelList; - static float nameColumnWidth = 250f; + static float nameColumnWidth = 150f; + static float templateColumnWidth = 100f; static float textColumnWidth = 150f; static float includeInBuildColumnWidth = 50f; - static float actionColumnWidth = 30f; + static float actionColumnWidth = 20f; static int elementPadding = 10; static GUIContent trashIcon; static List modelOptions; static List modelURLs; + string[] templateOptions; protected override Type[] GetPropertyTypes() { @@ -35,14 +37,12 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) { + float[] widths = GetColumnWidths(); + float listWidth = ReorderableList.Defaults.dragHandleWidth; + foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); + EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); modelList.DoLayoutList(); - string[] templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); - int index = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), llmScript.chatTemplate); - int newIndex = EditorGUILayout.Popup("Chat Template", index, templateOptions); - if (newIndex != index) - { - llmScript.SetTemplate(ChatTemplate.templatesDescription[templateOptions[newIndex]]); - } + EditorGUILayout.EndVertical(); } public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bool layout = true) @@ -70,14 +70,11 @@ public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bo public void AddModelSettings(SerializedObject llmScriptSO) { List attributeClasses = new List { typeof(ModelAttribute) }; - List excludeAttributeClasses = new List { typeof(ModelDownloadAttribute), typeof(ModelDownloadAdvancedAttribute) }; - if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) excludeAttributeClasses.Remove(typeof(ModelDownloadAttribute)); if (llmScriptSO.FindProperty("advancedOptions").boolValue) { attributeClasses.Add(typeof(ModelAdvancedAttribute)); - if (llmScriptSO.FindProperty("downloadOnBuild").boolValue) excludeAttributeClasses.Remove(typeof(ModelDownloadAdvancedAttribute)); } - ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false, excludeAttributeClasses); + ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false); Space(); } @@ -95,23 +92,29 @@ static void ResetModelOptions() for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) { string url = LLMUnitySetup.modelOptions[i].Item2; - if (existingOptions.Contains(url)) continue; + if (i > 0 && existingOptions.Contains(url)) continue; modelOptions.Add(LLMUnitySetup.modelOptions[i].Item1); modelURLs.Add(url); } } - List getColumnPositions(float offsetX) + float[] GetColumnWidths() + { + float[] widths = new float[] {actionColumnWidth, nameColumnWidth, templateColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth, actionColumnWidth}; + return widths; + } + + List CreateColumnRects(float x, float y) { - List offsets = new List(); - float[] widths = new float[] {actionColumnWidth, nameColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth}; - float offset = offsetX; + float[] widths = GetColumnWidths(); + float offset = x; + List rects = new List(); foreach (float width in widths) { - offsets.Add(offset); + rects.Add(new Rect(offset, y, width, EditorGUIUtility.singleLineHeight)); offset += width + elementPadding; } - return new List(){offsets.ToArray(), widths}; + return rects; } void UpdateModels(bool resetOptions = false) @@ -123,41 +126,54 @@ void UpdateModels(bool resetOptions = false) void OnEnable() { + var llmScript = (LLM)target; ResetModelOptions(); + templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), true, true, true, true) { - drawElementCallback = async(rect, index, isActive, isFocused) => + drawElementCallback = (rect, index, isActive, isFocused) => { if (index >= LLMManager.modelEntries.Count) return; - List positions = getColumnPositions(rect.x); - float[] offsets = positions[0]; - float[] widths = positions[1]; - var actionRect = new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight); - var nameRect = new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight); - var urlRect = new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight); - var pathRect = new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight); - var includeInBuildRect = new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight); + List rects = CreateColumnRects(rect.x, rect.y); + var selectRect = rects[0]; + var nameRect = rects[1]; + var templateRect = rects[2]; + var urlRect = rects[3]; + var pathRect = rects[4]; + var includeInBuildRect = rects[5]; + var actionRect = rects[6]; var entry = LLMManager.modelEntries[index]; bool hasPath = entry.localPath != null && entry.localPath != ""; bool hasURL = entry.url != null && entry.url != ""; - if (GUI.Button(actionRect, trashIcon)) + bool isSelected = llmScript.model == entry.localPath; + bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); + if (newSelected && !isSelected) { - LLMManager.modelEntries.Remove(entry); - UpdateModels(true); + llmScript.model = entry.localPath; + llmScript.SetTemplate(entry.chatTemplate); } DrawCopyableLabel(nameRect, entry.name); + int templateIndex = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), entry.chatTemplate); + int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateOptions); + if (newTemplateIndex != templateIndex) + { + entry.chatTemplate = ChatTemplate.templatesDescription[templateOptions[newTemplateIndex]]; + if (isSelected) llmScript.SetTemplate(entry.chatTemplate); + UpdateModels(); + } + if (hasURL) { DrawCopyableLabel(urlRect, entry.url); } - else if (hasPath) + else { string newURL = EditorGUI.TextField(urlRect, entry.url); if (newURL != entry.url) @@ -166,56 +182,63 @@ void OnEnable() UpdateModels(); } } - else + DrawCopyableLabel(pathRect, entry.localPath); + + bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); + if (includeInBuild != entry.includeInBuild) { - urlRect.width = buttonWidth; - int newIndex = EditorGUI.Popup(urlRect, 0, modelOptions.ToArray()); - if (newIndex != 0) - { - await LLMManager.DownloadModel(entry, modelURLs[newIndex], modelOptions[newIndex]); - UpdateModels(true); - } + entry.includeInBuild = includeInBuild; + UpdateModels(); } - if (hasPath) + if (GUI.Button(actionRect, trashIcon)) { - DrawCopyableLabel(pathRect, entry.localPath); + LLMManager.modelEntries.Remove(entry); + UpdateModels(true); } - else + }, + drawHeaderCallback = (rect) => + { + List rects = CreateColumnRects(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1, rect.y); + EditorGUI.LabelField(rects[0], ""); + EditorGUI.LabelField(rects[1], "Model"); + EditorGUI.LabelField(rects[2], "Chat template"); + EditorGUI.LabelField(rects[3], "URL"); + EditorGUI.LabelField(rects[4], "Path"); + EditorGUI.LabelField(rects[5], "Build"); + EditorGUI.LabelField(rects[6], ""); + }, + drawFooterCallback = async(rect) => + { + Rect downloadRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect loadRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + + int newIndex = EditorGUI.Popup(downloadRect, 0, modelOptions.ToArray()); + if (newIndex != 0) { - pathRect.width = buttonWidth; - if (GUI.Button(pathRect, "Load model")) + await LLMManager.DownloadModel(modelURLs[newIndex], modelOptions[newIndex]); + UpdateModels(true); + } + + if (GUI.Button(loadRect, "Load model")) + { + EditorApplication.delayCall += () => { - EditorApplication.delayCall += () => + string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); + if (!string.IsNullOrEmpty(path)) { - string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); - if (!string.IsNullOrEmpty(path)) - { - entry.localPath = path; - entry.name = LLMManager.ModelPathToName(path); - UpdateModels(); - } - }; - } + LLMManager.LoadModel(path); + UpdateModels(); + } + }; } - bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); - if (includeInBuild != entry.includeInBuild) + bool downloadOnBuild = EditorGUILayout.Toggle("Download on Build", LLMManager.downloadOnBuild); + if (downloadOnBuild != LLMManager.downloadOnBuild) { - entry.includeInBuild = includeInBuild; + LLMManager.downloadOnBuild = downloadOnBuild; UpdateModels(); } - }, - drawHeaderCallback = (rect) => - { - List positions = getColumnPositions(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1); - float[] offsets = positions[0]; - float[] widths = positions[1]; - EditorGUI.LabelField(new Rect(offsets[0], rect.y, widths[0], EditorGUIUtility.singleLineHeight), ""); - EditorGUI.LabelField(new Rect(offsets[1], rect.y, widths[1], EditorGUIUtility.singleLineHeight), "Model"); - EditorGUI.LabelField(new Rect(offsets[2], rect.y, widths[2], EditorGUIUtility.singleLineHeight), "URL"); - EditorGUI.LabelField(new Rect(offsets[3], rect.y, widths[3], EditorGUIUtility.singleLineHeight), "Local Path"); - EditorGUI.LabelField(new Rect(offsets[4], rect.y, widths[4], EditorGUIUtility.singleLineHeight), "Build"); } }; } @@ -250,14 +273,12 @@ public override void OnInspectorGUI() OnInspectorGUIStart(llmScriptSO); ShowProgress(LLMUnitySetup.libraryProgress, "Setup Library"); - ShowProgress(llmScript.modelProgress, "Model Downloading"); - ShowProgress(llmScript.modelCopyProgress, "Model Copying"); + ShowProgress(LLMManager.modelProgress, "Model Downloading"); + GUI.enabled = LLMUnitySetup.libraryProgress == 1 && LLMManager.modelProgress == 1; - GUI.enabled = LLMUnitySetup.libraryProgress == 1 && llmScript.modelProgress == 1 && llmScript.modelCopyProgress == 1; AddOptionsToggles(llmScriptSO); AddSetupSettings(llmScriptSO); AddModelLoadersSettings(llmScriptSO, llmScript); - GUI.enabled = true; AddChatSettings(llmScriptSO); OnInspectorGUIEnd(llmScriptSO); diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 1811b942..6af7ea3b 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -68,14 +68,6 @@ public class LLM : MonoBehaviour [LLMAdvanced] public bool asynchronousStartup = true; /// select to not destroy the LLM GameObject when loading a new Scene. [LLMAdvanced] public bool dontDestroyOnLoad = true; - /// toggle to enable model download on build - [Model] public bool downloadOnBuild = false; - /// the path of the model being used (relative to the Assets/StreamingAssets folder). - /// Models with .gguf format are allowed. - [Model] public string model = ""; - /// the URL of the model to use. - /// Models with .gguf format are allowed. - [ModelDownload] public string modelURL = ""; /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). /// Models with .bin format are allowed. [ModelAdvanced] public string lora = ""; @@ -90,20 +82,13 @@ public class LLM : MonoBehaviour /// a base prompt to use as a base for all LLMCharacter objects [TextArea(5, 10), ChatAdvanced] public string basePrompt = ""; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. - public bool modelsDownloaded { get; protected set; } = false; - /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; /// \cond HIDE public LLMManager llmManager = new LLMManager(); - public int SelectedModel = 0; - [HideInInspector] public float modelProgress = 1; - [HideInInspector] public float loraProgress = 1; - [HideInInspector] public float modelCopyProgress = 1; - [HideInInspector] public bool modelHide = true; - + public string model = ""; public string chatTemplate = ChatTemplate.DefaultTemplate; IntPtr LLMObject = IntPtr.Zero; @@ -112,93 +97,9 @@ public class LLM : MonoBehaviour StreamWrapper logStreamWrapper = null; Thread llmThread = null; List streamWrappers = new List(); - List> modelProgressCallbacks = new List>(); - List> loraProgressCallbacks = new List>(); - - public void SetModelProgress(float progress) - { - modelProgress = progress; - foreach (Callback modelProgressCallback in modelProgressCallbacks) modelProgressCallback?.Invoke(progress); - } - - public void SetLoraProgress(float progress) - { - loraProgress = progress; - foreach (Callback loraProgressCallback in loraProgressCallbacks) loraProgressCallback?.Invoke(progress); - } /// \endcond - string CopyAsset(string path) - { -#if UNITY_EDITOR - if (!EditorApplication.isPlaying) - { - modelCopyProgress = 0; - path = LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); - modelCopyProgress = 1; - } -#endif - return path; - } - - public void ResetSelectedModel() - { - SelectedModel = 0; - modelURL = ""; - model = ""; - } - - public async Task DownloadDefaultModel(int optionIndex) - { - // download default model and disable model editor properties until the model is set - if (optionIndex == 0) - { - ResetSelectedModel(); - return; - } - SelectedModel = optionIndex; - string modelUrl = LLMUnitySetup.modelOptions[optionIndex].Item2; - modelURL = modelUrl; - string modelName = Path.GetFileName(modelUrl).Split("?")[0]; - await DownloadModel(modelUrl, modelName); - } - - public async Task DownloadModel(string modelUrl, string modelName, bool overwrite = false, bool setTemplate = true) - { - modelProgress = 0; - string modelPath = LLMUnitySetup.GetAssetPath(modelName); - await LLMUnitySetup.DownloadFile(modelUrl, modelPath, overwrite, (string path) => SetModel(path, setTemplate), SetModelProgress); - } - - public async Task DownloadLora(string loraUrl, string loraName, bool overwrite = false) - { - loraProgress = 0; - string loraPath = LLMUnitySetup.GetAssetPath(loraName); - await LLMUnitySetup.DownloadFile(loraUrl, loraPath, overwrite, SetLora, SetLoraProgress); - } - - public async Task DownloadModels(bool overwrite = false) - { - if (modelURL != "") await DownloadModel(modelURL, model, overwrite, false); - if (loraURL != "") await DownloadLora(loraURL, lora, overwrite); - } - - public async Task AndroidExtractModels() - { - if (!downloadOnBuild || modelURL == "") await LLMUnitySetup.AndroidExtractFile(model); - if (!downloadOnBuild || loraURL == "") await LLMUnitySetup.AndroidExtractFile(lora); - } - - public async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) - { - if (modelProgressCallback != null) modelProgressCallbacks.Add(modelProgressCallback); - if (loraProgressCallback != null) loraProgressCallbacks.Add(loraProgressCallback); - while (!modelsDownloaded) await Task.Yield(); - if (modelProgressCallback != null) modelProgressCallbacks.Remove(modelProgressCallback); - if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); - } - public async Task WaitUntilReady() { while (!started) await Task.Yield(); @@ -210,13 +111,16 @@ public async Task WaitUntilReady() /// Models supported are in .gguf format. /// /// path to model to use (.gguf format) - public void SetModel(string path, bool setTemplate = true) + public void SetModel(string path) { // set the model and enable the model editor properties - model = CopyAsset(path); - if (setTemplate) SetTemplate(ChatTemplate.FromGGUF(LLMUnitySetup.GetAssetPath(model))); #if UNITY_EDITOR + ModelEntry entry = LLMManager.LoadModel(path); + model = entry.localPath; + SetTemplate(entry.chatTemplate); if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); +#else + model = path; #endif } @@ -228,7 +132,7 @@ public void SetModel(string path, bool setTemplate = true) /// path to LORA model to use (.bin format) public void SetLora(string path) { - lora = CopyAsset(path); + lora = path; #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif @@ -297,9 +201,9 @@ protected virtual string GetLlamaccpArguments() public async void Awake() { if (!enabled) return; - if (downloadOnBuild) await DownloadModels(); - modelsDownloaded = true; - if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); + // if (downloadOnBuild) await DownloadModels(); + // modelsDownloaded = true; + // if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); string arguments = GetLlamaccpArguments(); if (arguments == null) return; if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 2496a8b6..d18cebfe 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -12,21 +12,32 @@ namespace LLMUnity public class ModelEntry { public string name; + public string chatTemplate; public string url; public string localPath; public bool includeInBuild; } [Serializable] - public class ModelEntryList + public class LLMManagerStore { + public bool downloadOnBuild; public List modelEntries; } public class LLMManager { + public static bool downloadOnBuild = false; public static List modelEntries = new List(); + /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. + public static bool modelsDownloaded { get; protected set; } = false; + static List> modelProgressCallbacks = new List>(); + static List> loraProgressCallbacks = new List>(); + + [HideInInspector] public static float modelProgress = 1; + // [HideInInspector] public static float loraProgress = 1; + [InitializeOnLoadMethod] static void InitializeOnLoad() { @@ -38,26 +49,73 @@ public static string ModelPathToName(string path) return Path.GetFileNameWithoutExtension(path.Split("?")[0]); } - public static async Task DownloadModel(ModelEntry entry, string url, string name = null) + public static ModelEntry CreateEntry(string path, string url = null, string name = null) { - string modelName = Path.GetFileName(url).Split("?")[0]; - string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); - await LLMUnitySetup.DownloadFile(url, modelPath); + ModelEntry entry = new ModelEntry(); entry.name = name == null ? ModelPathToName(url) : name; + entry.chatTemplate = ChatTemplate.FromGGUF(path); entry.url = url; - entry.localPath = modelPath; + entry.localPath = Path.GetFullPath(path).Replace('\\', '/'); + return entry; + } + + public static ModelEntry AddEntry(string path, string url = null, string name = null) + { + ModelEntry entry = CreateEntry(path, url, name); + modelEntries.Add(entry); + return entry; + } + + public static async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) + { + if (modelProgressCallback != null) modelProgressCallbacks.Add(modelProgressCallback); + if (loraProgressCallback != null) loraProgressCallbacks.Add(loraProgressCallback); + while (!modelsDownloaded) await Task.Yield(); + if (modelProgressCallback != null) modelProgressCallbacks.Remove(modelProgressCallback); + if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); + } + + public static async Task DownloadModel(string url, string name = null) + { + foreach (ModelEntry modelEntry in modelEntries) + { + if (modelEntry.url == url) return modelEntry; + } + string modelName = Path.GetFileName(url).Split("?")[0]; + string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); + modelProgress = 0; + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetModelProgress); + return AddEntry(modelPath, url, name); + } + + public static ModelEntry LoadModel(string path) + { + string fullPath = Path.GetFullPath(path).Replace('\\', '/'); + foreach (ModelEntry modelEntry in modelEntries) + { + if (modelEntry.localPath == fullPath) return modelEntry; + } + return AddEntry(path); + } + + public static void SetModelProgress(float progress) + { + modelProgress = progress; + foreach (Callback modelProgressCallback in modelProgressCallbacks) modelProgressCallback?.Invoke(progress); } public static void Save() { Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); - File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new ModelEntryList { modelEntries = modelEntries })); + File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnBuild = downloadOnBuild })); } public static void Load() { if (!File.Exists(LLMUnitySetup.modelListPath)) return; - modelEntries = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)).modelEntries; + LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)); + modelEntries = store.modelEntries; + downloadOnBuild = store.downloadOnBuild; } } } diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index a5184093..b9dd8f2d 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -258,6 +258,13 @@ public static async Task AndroidExtractFile(string assetName, bool overwrite = f } } + public static bool IsSubPath(string childPath, string parentPath) + { + string fullParentPath = Path.GetFullPath(parentPath).Replace('\\', '/'); + string fullChildPath = Path.GetFullPath(childPath).Replace('\\', '/'); + return fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase); + } + #if UNITY_EDITOR [HideInInspector] public static float libraryProgress = 1; From 74ecfd4674acdba3f62d19df0ea1283b7afd34a7 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 24 Jul 2024 15:54:29 +0300 Subject: [PATCH 036/105] add custom url option --- Runtime/LLMUnitySetup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index b9dd8f2d..2ebce5a7 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -87,6 +87,7 @@ public class LLMUnitySetup [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] { ("Download model", null), + ("Custom URL", null), ("Mistral 7B Instruct v0.2 (medium, best overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), ("OpenHermes 2.5 7B (medium, best for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), ("Phi 3 (small, great)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), From 8fb57c59f8c8df191d5fae99cbb1a3762b6cb51a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 24 Jul 2024 15:55:13 +0300 Subject: [PATCH 037/105] implement loras to model selection --- Editor/LLMEditor.cs | 223 ++++++++++++++++++++++++++++-------------- Runtime/LLM.cs | 2 +- Runtime/LLMManager.cs | 138 +++++++++++++++++++++----- 3 files changed, 263 insertions(+), 100 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 63f95455..7f4196c5 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using UnityEditor; using UnityEditorInternal; using UnityEngine; @@ -21,6 +22,11 @@ public class LLMEditor : PropertyEditor static List modelOptions; static List modelURLs; string[] templateOptions; + string elementFocus = ""; + bool showCustomURL = false; + string customURL = ""; + bool customURLLora = false; + bool customURLFocus = false; protected override Type[] GetPropertyTypes() { @@ -31,7 +37,6 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) { EditorGUILayout.LabelField("Model Settings", EditorStyles.boldLabel); AddModelLoaders(llmScriptSO, llmScript); - AddModelAddonLoaders(llmScriptSO, llmScript); AddModelSettings(llmScriptSO); } @@ -42,29 +47,13 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); modelList.DoLayoutList(); - EditorGUILayout.EndVertical(); - } - - public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bool layout = true) - { - if (llmScriptSO.FindProperty("advancedOptions").boolValue) + bool downloadOnBuild = EditorGUILayout.Toggle("Download on Build", LLMManager.downloadOnBuild); + if (downloadOnBuild != LLMManager.downloadOnBuild) { - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("Lora", GUILayout.Width(EditorGUIUtility.labelWidth)); - - if (GUILayout.Button("Load lora", GUILayout.Width(buttonWidth))) - { - EditorApplication.delayCall += () => - { - string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); - if (!string.IsNullOrEmpty(path)) - { - llmScript.SetLora(path); - } - }; - } - EditorGUILayout.EndHorizontal(); + LLMManager.downloadOnBuild = downloadOnBuild; + LLMManager.Save(); } + EditorGUILayout.EndVertical(); } public void AddModelSettings(SerializedObject llmScriptSO) @@ -89,11 +78,10 @@ static void ResetModelOptions() foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); modelOptions = new List(); modelURLs = new List(); - for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) + foreach ((string name, string url) in LLMUnitySetup.modelOptions) { - string url = LLMUnitySetup.modelOptions[i].Item2; - if (i > 0 && existingOptions.Contains(url)) continue; - modelOptions.Add(LLMUnitySetup.modelOptions[i].Item1); + if (url != null && existingOptions.Contains(url)) continue; + modelOptions.Add(name); modelURLs.Add(url); } } @@ -104,14 +92,14 @@ float[] GetColumnWidths() return widths; } - List CreateColumnRects(float x, float y) + List CreateColumnRects(Rect rect) { float[] widths = GetColumnWidths(); - float offset = x; + float offset = rect.x; List rects = new List(); foreach (float width in widths) { - rects.Add(new Rect(offset, y, width, EditorGUIUtility.singleLineHeight)); + rects.Add(new Rect(offset, rect.y, width, EditorGUIUtility.singleLineHeight)); offset += width + elementPadding; } return rects; @@ -124,20 +112,112 @@ void UpdateModels(bool resetOptions = false) Repaint(); } + void showCustomURLField(bool lora) + { + customURL = ""; + customURLLora = lora; + showCustomURL = true; + customURLFocus = true; + Repaint(); + } + + async Task createCustomURLField(Rect rect) + { + bool submit; + Event e = Event.current; + if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)) + { + submit = true; + e.Use(); + } + else + { + Rect labelRect = new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight); + Rect textRect = new Rect(rect.x + labelRect.width + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect submitRect = new Rect(rect.x + labelRect.width + buttonWidth + elementPadding * 2 , rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + + EditorGUI.LabelField(labelRect, "Enter URL:"); + GUI.SetNextControlName("customURLFocus"); + customURL = EditorGUI.TextField(textRect, customURL); + submit = GUI.Button(submitRect, "Submit"); + + if (customURLFocus) + { + customURLFocus = false; + elementFocus = "customURLFocus"; + } + } + + if (submit) + { + showCustomURL = false; + elementFocus = "dummy"; + Repaint(); + await LLMManager.Download(customURL, customURLLora); + UpdateModels(true); + } + } + + async Task createButtons(Rect rect, LLM llmScript) + { + Rect downloadModelRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect loadModelRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect downloadLoraRect = new Rect(rect.x + (buttonWidth + elementPadding) * 2, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect loadLoraRect = new Rect(rect.x + (buttonWidth + elementPadding) * 3, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); + + if (modelIndex == 1) + { + showCustomURLField(false); + } + else if (modelIndex > 1) + { + await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); + UpdateModels(true); + } + + if (GUI.Button(loadModelRect, "Load model")) + { + EditorApplication.delayCall += () => + { + string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); + if (!string.IsNullOrEmpty(path)) + { + LLMManager.LoadModel(path); + UpdateModels(); + } + }; + } + + if (GUI.Button(downloadLoraRect, "Download LoRA")) + { + showCustomURLField(true); + } + if (GUI.Button(loadLoraRect, "Load LoRA")) + { + EditorApplication.delayCall += () => + { + string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); + if (!string.IsNullOrEmpty(path)) + { + llmScript.SetLora(path); + } + }; + } + } + void OnEnable() { - var llmScript = (LLM)target; + LLM llmScript = (LLM)target; ResetModelOptions(); templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); - - modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), true, true, true, true) + modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), false, true, false, false) { drawElementCallback = (rect, index, isActive, isFocused) => { if (index >= LLMManager.modelEntries.Count) return; - List rects = CreateColumnRects(rect.x, rect.y); + List rects = CreateColumnRects(rect); var selectRect = rects[0]; var nameRect = rects[1]; var templateRect = rects[2]; @@ -150,23 +230,34 @@ void OnEnable() bool hasPath = entry.localPath != null && entry.localPath != ""; bool hasURL = entry.url != null && entry.url != ""; - bool isSelected = llmScript.model == entry.localPath; - bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); - if (newSelected && !isSelected) + bool isSelected = false; + if (!entry.lora) { - llmScript.model = entry.localPath; - llmScript.SetTemplate(entry.chatTemplate); + isSelected = llmScript.model == entry.localPath; + bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); + if (newSelected && !isSelected) llmScript.SetModel(entry.localPath); } + else + { + isSelected = llmScript.lora == entry.localPath; + bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); + if (newSelected && !isSelected) llmScript.SetLora(entry.localPath); + else if (!newSelected && isSelected) llmScript.SetLora(""); + } + DrawCopyableLabel(nameRect, entry.name); - int templateIndex = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), entry.chatTemplate); - int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateOptions); - if (newTemplateIndex != templateIndex) + if (!entry.lora) { - entry.chatTemplate = ChatTemplate.templatesDescription[templateOptions[newTemplateIndex]]; - if (isSelected) llmScript.SetTemplate(entry.chatTemplate); - UpdateModels(); + int templateIndex = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), entry.chatTemplate); + int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateOptions); + if (newTemplateIndex != templateIndex) + { + entry.chatTemplate = ChatTemplate.templatesDescription[templateOptions[newTemplateIndex]]; + if (isSelected) llmScript.SetTemplate(entry.chatTemplate); + UpdateModels(); + } } if (hasURL) @@ -193,13 +284,13 @@ void OnEnable() if (GUI.Button(actionRect, trashIcon)) { - LLMManager.modelEntries.Remove(entry); + LLMManager.Remove(entry); UpdateModels(true); } }, drawHeaderCallback = (rect) => { - List rects = CreateColumnRects(rect.x + ReorderableList.Defaults.dragHandleWidth - ReorderableList.Defaults.padding + 1, rect.y); + List rects = CreateColumnRects(rect); EditorGUI.LabelField(rects[0], ""); EditorGUI.LabelField(rects[1], "Model"); EditorGUI.LabelField(rects[2], "Chat template"); @@ -210,35 +301,8 @@ void OnEnable() }, drawFooterCallback = async(rect) => { - Rect downloadRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect loadRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - - int newIndex = EditorGUI.Popup(downloadRect, 0, modelOptions.ToArray()); - if (newIndex != 0) - { - await LLMManager.DownloadModel(modelURLs[newIndex], modelOptions[newIndex]); - UpdateModels(true); - } - - if (GUI.Button(loadRect, "Load model")) - { - EditorApplication.delayCall += () => - { - string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); - if (!string.IsNullOrEmpty(path)) - { - LLMManager.LoadModel(path); - UpdateModels(); - } - }; - } - - bool downloadOnBuild = EditorGUILayout.Toggle("Download on Build", LLMManager.downloadOnBuild); - if (downloadOnBuild != LLMManager.downloadOnBuild) - { - LLMManager.downloadOnBuild = downloadOnBuild; - UpdateModels(); - } + if (showCustomURL) await createCustomURLField(rect); + else await createButtons(rect, llmScript); } }; } @@ -267,6 +331,12 @@ private void CopyToClipboard(string text) public override void OnInspectorGUI() { + if (elementFocus != "") + { + EditorGUI.FocusTextInControl(elementFocus); + elementFocus = ""; + } + LLM llmScript = (LLM)target; SerializedObject llmScriptSO = new SerializedObject(llmScript); @@ -274,7 +344,8 @@ public override void OnInspectorGUI() ShowProgress(LLMUnitySetup.libraryProgress, "Setup Library"); ShowProgress(LLMManager.modelProgress, "Model Downloading"); - GUI.enabled = LLMUnitySetup.libraryProgress == 1 && LLMManager.modelProgress == 1; + ShowProgress(LLMManager.loraProgress, "LoRA Downloading"); + GUI.enabled = LLMUnitySetup.libraryProgress == 1 && LLMManager.modelProgress == 1 && LLMManager.loraProgress == 1; AddOptionsToggles(llmScriptSO); AddSetupSettings(llmScriptSO); diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 6af7ea3b..a68f338d 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -115,7 +115,7 @@ public void SetModel(string path) { // set the model and enable the model editor properties #if UNITY_EDITOR - ModelEntry entry = LLMManager.LoadModel(path); + ModelEntry entry = LLMManager.Get(LLMManager.LoadModel(path)); model = entry.localPath; SetTemplate(entry.chatTemplate); if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index d18cebfe..1e928ded 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using UnityEditor; using UnityEngine; @@ -12,6 +13,7 @@ namespace LLMUnity public class ModelEntry { public string name; + public bool lora; public string chatTemplate; public string url; public string localPath; @@ -36,7 +38,7 @@ public class LLMManager static List> loraProgressCallbacks = new List>(); [HideInInspector] public static float modelProgress = 1; - // [HideInInspector] public static float loraProgress = 1; + [HideInInspector] public static float loraProgress = 1; [InitializeOnLoadMethod] static void InitializeOnLoad() @@ -49,21 +51,29 @@ public static string ModelPathToName(string path) return Path.GetFileNameWithoutExtension(path.Split("?")[0]); } - public static ModelEntry CreateEntry(string path, string url = null, string name = null) + public static string AddEntry(string path, bool lora = false, string name = null, string url = null) { + string key = name == null ? ModelPathToName(url) : name; ModelEntry entry = new ModelEntry(); - entry.name = name == null ? ModelPathToName(url) : name; - entry.chatTemplate = ChatTemplate.FromGGUF(path); + entry.name = key; + entry.lora = lora; + entry.chatTemplate = lora ? null : ChatTemplate.FromGGUF(path); entry.url = url; entry.localPath = Path.GetFullPath(path).Replace('\\', '/'); - return entry; - } - - public static ModelEntry AddEntry(string path, string url = null, string name = null) - { - ModelEntry entry = CreateEntry(path, url, name); - modelEntries.Add(entry); - return entry; + int indexToInsert = modelEntries.Count; + if (!lora) + { + for (int i = modelEntries.Count - 1; i >= 0; i--) + { + if (!modelEntries[i].lora) + { + indexToInsert = i + 1; + break; + } + } + } + modelEntries.Insert(indexToInsert, entry); + return key; } public static async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) @@ -75,27 +85,103 @@ public static async Task WaitUntilModelsDownloaded(Callback modelProgress if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); } - public static async Task DownloadModel(string url, string name = null) + public static async Task Download(string url, bool lora = false, string name = null) { - foreach (ModelEntry modelEntry in modelEntries) + foreach (ModelEntry entry in modelEntries) { - if (modelEntry.url == url) return modelEntry; + if (entry.url == url) return entry.name; } string modelName = Path.GetFileName(url).Split("?")[0]; string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); - modelProgress = 0; - await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetModelProgress); - return AddEntry(modelPath, url, name); + if (!lora) + { + modelProgress = 0; + try + { + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetModelProgress); + } + catch (Exception ex) + { + modelProgress = 1; + throw ex; + } + } + else + { + loraProgress = 0; + try + { + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetLoraProgress); + } + catch (Exception ex) + { + loraProgress = 1; + throw ex; + } + } + return AddEntry(modelPath, lora, name, url); } - public static ModelEntry LoadModel(string path) + public static string Load(string path, bool lora = false, string name = null) { string fullPath = Path.GetFullPath(path).Replace('\\', '/'); - foreach (ModelEntry modelEntry in modelEntries) + foreach (ModelEntry entry in modelEntries) + { + if (entry.localPath == fullPath) return entry.name; + } + return AddEntry(path, lora, name); + } + + public static async Task DownloadModel(string url, string name = null) + { + return await Download(url, false, name); + } + + public static async Task DownloadLora(string url, string name = null) + { + return await Download(url, true, name); + } + + public static string LoadModel(string url, string name = null) + { + return Load(url, false, name); + } + + public static string LoadLora(string url, string name = null) + { + return Load(url, true, name); + } + + public static void SetModelTemplate(string name, string chatTemplate) + { + foreach (ModelEntry entry in modelEntries) { - if (modelEntry.localPath == fullPath) return modelEntry; + if (entry.name == name) + { + entry.chatTemplate = chatTemplate; + break; + } } - return AddEntry(path); + } + + public static ModelEntry Get(string name) + { + foreach (ModelEntry entry in modelEntries) + { + if (entry.name == name) return entry; + } + return null; + } + + public static void Remove(string name) + { + Remove(Get(name)); + } + + public static void Remove(ModelEntry entry) + { + if (entry == null) return; + modelEntries.Remove(entry); } public static void SetModelProgress(float progress) @@ -104,6 +190,12 @@ public static void SetModelProgress(float progress) foreach (Callback modelProgressCallback in modelProgressCallbacks) modelProgressCallback?.Invoke(progress); } + public static void SetLoraProgress(float progress) + { + loraProgress = progress; + foreach (Callback loraProgressCallback in loraProgressCallbacks) loraProgressCallback?.Invoke(progress); + } + public static void Save() { Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); @@ -114,8 +206,8 @@ public static void Load() { if (!File.Exists(LLMUnitySetup.modelListPath)) return; LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)); - modelEntries = store.modelEntries; downloadOnBuild = store.downloadOnBuild; + modelEntries = store.modelEntries; } } } From 9658c7728adc45293e4e33710df0ee45afec8041 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 24 Jul 2024 16:56:31 +0300 Subject: [PATCH 038/105] json and button beautification --- Editor/LLMEditor.cs | 5 +++-- Runtime/LLMManager.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 7f4196c5..f1febad4 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -162,9 +162,10 @@ async Task createButtons(Rect rect, LLM llmScript) { Rect downloadModelRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); Rect loadModelRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect downloadLoraRect = new Rect(rect.x + (buttonWidth + elementPadding) * 2, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect loadLoraRect = new Rect(rect.x + (buttonWidth + elementPadding) * 3, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); + Rect downloadLoraRect = new Rect(rect.width - 2 * buttonWidth - elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect loadLoraRect = new Rect(rect.width - buttonWidth, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); if (modelIndex == 1) { showCustomURLField(false); diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 1e928ded..073f03f1 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -199,7 +199,7 @@ public static void SetLoraProgress(float progress) public static void Save() { Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); - File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnBuild = downloadOnBuild })); + File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnBuild = downloadOnBuild }, true)); } public static void Load() From 560e58a4d8107b5c8d1d48054d698c07a3550520 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 24 Jul 2024 20:26:08 +0300 Subject: [PATCH 039/105] lora as argument --- Editor/LLMEditor.cs | 22 ++++++++++++++++------ Runtime/LLM.cs | 7 +------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index f1febad4..9873968e 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -95,12 +95,13 @@ float[] GetColumnWidths() List CreateColumnRects(Rect rect) { float[] widths = GetColumnWidths(); - float offset = rect.x; + float offsetX = rect.x; + float offsetY = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2; List rects = new List(); foreach (float width in widths) { - rects.Add(new Rect(offset, rect.y, width, EditorGUIUtility.singleLineHeight)); - offset += width + elementPadding; + rects.Add(new Rect(offsetX, offsetY, width, EditorGUIUtility.singleLineHeight)); + offsetX += width + elementPadding; } return rects; } @@ -162,8 +163,8 @@ async Task createButtons(Rect rect, LLM llmScript) { Rect downloadModelRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); Rect loadModelRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect downloadLoraRect = new Rect(rect.width - 2 * buttonWidth - elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect loadLoraRect = new Rect(rect.width - buttonWidth, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect downloadLoraRect = new Rect(rect.xMax - 2 * buttonWidth - elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect loadLoraRect = new Rect(rect.xMax - buttonWidth, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); if (modelIndex == 1) @@ -212,11 +213,16 @@ void OnEnable() ResetModelOptions(); templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); + Texture2D loraLineTexture = new Texture2D(1, 1); + loraLineTexture.SetPixel(0, 0, Color.black); + loraLineTexture.Apply(); + modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), false, true, false, false) { drawElementCallback = (rect, index, isActive, isFocused) => { if (index >= LLMManager.modelEntries.Count) return; + var entry = LLMManager.modelEntries[index]; List rects = CreateColumnRects(rect); var selectRect = rects[0]; @@ -226,7 +232,6 @@ void OnEnable() var pathRect = rects[4]; var includeInBuildRect = rects[5]; var actionRect = rects[6]; - var entry = LLMManager.modelEntries[index]; bool hasPath = entry.localPath != null && entry.localPath != ""; bool hasURL = entry.url != null && entry.url != ""; @@ -288,6 +293,11 @@ void OnEnable() LLMManager.Remove(entry); UpdateModels(true); } + + if (!entry.lora && index < LLMManager.modelEntries.Count - 1 && LLMManager.modelEntries[index + 1].lora) + { + GUI.DrawTexture(new Rect(rect.x - ReorderableList.Defaults.padding, rect.yMax, rect.width + ReorderableList.Defaults.padding * 2, 1), loraLineTexture); + } }, drawHeaderCallback = (rect) => { diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index a68f338d..87d48fa2 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -68,12 +68,6 @@ public class LLM : MonoBehaviour [LLMAdvanced] public bool asynchronousStartup = true; /// select to not destroy the LLM GameObject when loading a new Scene. [LLMAdvanced] public bool dontDestroyOnLoad = true; - /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). - /// Models with .bin format are allowed. - [ModelAdvanced] public string lora = ""; - /// the URL of the LORA to use. - /// Models with .bin format are allowed. - [ModelDownloadAdvanced] public string loraURL = ""; /// Size of the prompt context (0 = context size of the model). /// This is the number of tokens the model can take as input when generating responses. [ModelAdvanced] public int contextSize = 0; @@ -88,6 +82,7 @@ public class LLM : MonoBehaviour /// \cond HIDE public LLMManager llmManager = new LLMManager(); + public string lora = ""; public string model = ""; public string chatTemplate = ChatTemplate.DefaultTemplate; From beda63f1aaff26bb8d0d7582bda58c38366419a0 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 24 Jul 2024 20:26:49 +0300 Subject: [PATCH 040/105] UI improvements --- Editor/LLMEditor.cs | 184 ++++++++++++++++++++++++++---------------- Runtime/LLMManager.cs | 42 +++++----- 2 files changed, 132 insertions(+), 94 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 9873968e..3a160d70 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -13,9 +13,9 @@ public class LLMEditor : PropertyEditor { private ReorderableList modelList; static float nameColumnWidth = 150f; - static float templateColumnWidth = 100f; + static float templateColumnWidth = 150f; static float textColumnWidth = 150f; - static float includeInBuildColumnWidth = 50f; + static float includeInBuildColumnWidth = 30f; static float actionColumnWidth = 20f; static int elementPadding = 10; static GUIContent trashIcon; @@ -42,18 +42,25 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) { - float[] widths = GetColumnWidths(); - float listWidth = ReorderableList.Defaults.dragHandleWidth; - foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); - EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); - modelList.DoLayoutList(); - bool downloadOnBuild = EditorGUILayout.Toggle("Download on Build", LLMManager.downloadOnBuild); - if (downloadOnBuild != LLMManager.downloadOnBuild) + if (LLMManager.modelEntries.Count == 0) { - LLMManager.downloadOnBuild = downloadOnBuild; + DrawFooter(EditorGUILayout.GetControlRect()); + } + else + { + float[] widths = GetColumnWidths(llmScript.advancedOptions); + float listWidth = 2 * ReorderableList.Defaults.padding * 2; + foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); + EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); + modelList.DoLayoutList(); + EditorGUILayout.EndVertical(); + } + bool downloadOnStart = EditorGUILayout.Toggle("Download on Start", LLMManager.downloadOnStart); + if (downloadOnStart != LLMManager.downloadOnStart) + { + LLMManager.downloadOnStart = downloadOnStart; LLMManager.Save(); } - EditorGUILayout.EndVertical(); } public void AddModelSettings(SerializedObject llmScriptSO) @@ -86,15 +93,17 @@ static void ResetModelOptions() } } - float[] GetColumnWidths() + float[] GetColumnWidths(bool expandedView) { - float[] widths = new float[] {actionColumnWidth, nameColumnWidth, templateColumnWidth, textColumnWidth, textColumnWidth, includeInBuildColumnWidth, actionColumnWidth}; - return widths; + List widths = new List(){actionColumnWidth, nameColumnWidth, templateColumnWidth}; + if (expandedView) widths.AddRange(new List(){textColumnWidth, textColumnWidth}); + widths.AddRange(new List(){includeInBuildColumnWidth, actionColumnWidth}); + return widths.ToArray(); } - List CreateColumnRects(Rect rect) + List CreateColumnRects(Rect rect, bool expandedView) { - float[] widths = GetColumnWidths(); + float[] widths = GetColumnWidths(expandedView); float offsetX = rect.x; float offsetY = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2; List rects = new List(); @@ -122,25 +131,39 @@ void showCustomURLField(bool lora) Repaint(); } + void SetModelIfNone() + { + LLM llmScript = (LLM)target; + if (llmScript.model == "" && LLMManager.modelEntries.Count == 1) llmScript.SetModel(LLMManager.modelEntries[0].localPath); + } + async Task createCustomURLField(Rect rect) { - bool submit; + bool submit = false; + bool exit = false; Event e = Event.current; if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)) { submit = true; e.Use(); } + else if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Escape)) + { + exit = true; + e.Use(); + } else { Rect labelRect = new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight); Rect textRect = new Rect(rect.x + labelRect.width + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect submitRect = new Rect(rect.x + labelRect.width + buttonWidth + elementPadding * 2 , rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); + Rect submitRect = new Rect(rect.x + labelRect.width + buttonWidth + elementPadding * 2, rect.y, buttonWidth / 2f, EditorGUIUtility.singleLineHeight); + Rect backRect = new Rect(rect.x + labelRect.width + buttonWidth * 1.5f + elementPadding * 3, rect.y, buttonWidth / 2f, EditorGUIUtility.singleLineHeight); EditorGUI.LabelField(labelRect, "Enter URL:"); GUI.SetNextControlName("customURLFocus"); customURL = EditorGUI.TextField(textRect, customURL); submit = GUI.Button(submitRect, "Submit"); + exit = GUI.Button(backRect, "Back"); if (customURLFocus) { @@ -149,13 +172,17 @@ async Task createCustomURLField(Rect rect) } } - if (submit) + if (exit || submit) { showCustomURL = false; elementFocus = "dummy"; Repaint(); - await LLMManager.Download(customURL, customURLLora); - UpdateModels(true); + if (submit && customURL != "") + { + await LLMManager.Download(customURL, customURLLora); + SetModelIfNone(); + UpdateModels(true); + } } } @@ -165,7 +192,6 @@ async Task createButtons(Rect rect, LLM llmScript) Rect loadModelRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); Rect downloadLoraRect = new Rect(rect.xMax - 2 * buttonWidth - elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); Rect loadLoraRect = new Rect(rect.xMax - buttonWidth, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); if (modelIndex == 1) { @@ -174,6 +200,7 @@ async Task createButtons(Rect rect, LLM llmScript) else if (modelIndex > 1) { await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); + SetModelIfNone(); UpdateModels(true); } @@ -190,23 +217,33 @@ async Task createButtons(Rect rect, LLM llmScript) }; } - if (GUI.Button(downloadLoraRect, "Download LoRA")) - { - showCustomURLField(true); - } - if (GUI.Button(loadLoraRect, "Load LoRA")) + if (llmScript.advancedOptions) { - EditorApplication.delayCall += () => + if (GUI.Button(downloadLoraRect, "Download LoRA")) { - string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); - if (!string.IsNullOrEmpty(path)) + showCustomURLField(true); + } + if (GUI.Button(loadLoraRect, "Load LoRA")) + { + EditorApplication.delayCall += () => { - llmScript.SetLora(path); - } - }; + string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); + if (!string.IsNullOrEmpty(path)) + { + llmScript.SetLora(path); + } + }; + } } } + async void DrawFooter(Rect rect) + { + LLM llmScript = (LLM)target; + if (showCustomURL) await createCustomURLField(rect); + else await createButtons(rect, llmScript); + } + void OnEnable() { LLM llmScript = (LLM)target; @@ -222,16 +259,22 @@ void OnEnable() drawElementCallback = (rect, index, isActive, isFocused) => { if (index >= LLMManager.modelEntries.Count) return; - var entry = LLMManager.modelEntries[index]; - - List rects = CreateColumnRects(rect); - var selectRect = rects[0]; - var nameRect = rects[1]; - var templateRect = rects[2]; - var urlRect = rects[3]; - var pathRect = rects[4]; - var includeInBuildRect = rects[5]; - var actionRect = rects[6]; + ModelEntry entry = LLMManager.modelEntries[index]; + + List rects = CreateColumnRects(rect, llmScript.advancedOptions); + int col = 0; + Rect selectRect = rects[col++]; + Rect nameRect = rects[col++]; + Rect templateRect = rects[col++]; + Rect urlRect = new Rect(); + Rect pathRect = new Rect(); + if (llmScript.advancedOptions) + { + urlRect = rects[col++]; + pathRect = rects[col++]; + } + Rect includeInBuildRect = rects[col++]; + Rect actionRect = rects[col++]; bool hasPath = entry.localPath != null && entry.localPath != ""; bool hasURL = entry.url != null && entry.url != ""; @@ -251,7 +294,6 @@ void OnEnable() else if (!newSelected && isSelected) llmScript.SetLora(""); } - DrawCopyableLabel(nameRect, entry.name); if (!entry.lora) @@ -266,20 +308,23 @@ void OnEnable() } } - if (hasURL) + if (llmScript.advancedOptions) { - DrawCopyableLabel(urlRect, entry.url); - } - else - { - string newURL = EditorGUI.TextField(urlRect, entry.url); - if (newURL != entry.url) + if (hasURL) { - entry.url = newURL; - UpdateModels(); + DrawCopyableLabel(urlRect, entry.url); } + else + { + string newURL = EditorGUI.TextField(urlRect, entry.url); + if (newURL != entry.url) + { + entry.url = newURL; + UpdateModels(); + } + } + DrawCopyableLabel(pathRect, entry.localPath); } - DrawCopyableLabel(pathRect, entry.localPath); bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); if (includeInBuild != entry.includeInBuild) @@ -301,20 +346,20 @@ void OnEnable() }, drawHeaderCallback = (rect) => { - List rects = CreateColumnRects(rect); - EditorGUI.LabelField(rects[0], ""); - EditorGUI.LabelField(rects[1], "Model"); - EditorGUI.LabelField(rects[2], "Chat template"); - EditorGUI.LabelField(rects[3], "URL"); - EditorGUI.LabelField(rects[4], "Path"); - EditorGUI.LabelField(rects[5], "Build"); - EditorGUI.LabelField(rects[6], ""); + List rects = CreateColumnRects(rect, llmScript.advancedOptions); + int col = 0; + EditorGUI.LabelField(rects[col++], ""); + EditorGUI.LabelField(rects[col++], "Model"); + EditorGUI.LabelField(rects[col++], "Chat template"); + if (llmScript.advancedOptions) + { + EditorGUI.LabelField(rects[col++], "URL"); + EditorGUI.LabelField(rects[col++], "Path"); + } + EditorGUI.LabelField(rects[col++], "Build"); + EditorGUI.LabelField(rects[col++], ""); }, - drawFooterCallback = async(rect) => - { - if (showCustomURL) await createCustomURLField(rect); - else await createButtons(rect, llmScript); - } + drawFooterCallback = DrawFooter, }; } @@ -332,10 +377,7 @@ private void DrawCopyableLabel(Rect rect, string text) private void CopyToClipboard(string text) { - TextEditor te = new TextEditor - { - text = text - }; + TextEditor te = new TextEditor {text = text}; te.SelectAll(); te.Copy(); } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 073f03f1..d1469ab2 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using UnityEditor; using UnityEngine; @@ -23,13 +22,13 @@ public class ModelEntry [Serializable] public class LLMManagerStore { - public bool downloadOnBuild; + public bool downloadOnStart; public List modelEntries; } public class LLMManager { - public static bool downloadOnBuild = false; + public static bool downloadOnStart = false; public static List modelEntries = new List(); /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. @@ -53,13 +52,14 @@ public static string ModelPathToName(string path) public static string AddEntry(string path, bool lora = false, string name = null, string url = null) { - string key = name == null ? ModelPathToName(url) : name; + string key = name == null ? ModelPathToName(path) : name; ModelEntry entry = new ModelEntry(); entry.name = key; entry.lora = lora; entry.chatTemplate = lora ? null : ChatTemplate.FromGGUF(path); entry.url = url; entry.localPath = Path.GetFullPath(path).Replace('\\', '/'); + entry.includeInBuild = true; int indexToInsert = modelEntries.Count; if (!lora) { @@ -93,31 +93,27 @@ public static async Task Download(string url, bool lora = false, string } string modelName = Path.GetFileName(url).Split("?")[0]; string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); - if (!lora) + float preModelProgress = modelProgress; + float preLoraProgress = loraProgress; + try { - modelProgress = 0; - try + if (!lora) { + modelProgress = 0; await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetModelProgress); } - catch (Exception ex) + else { - modelProgress = 1; - throw ex; + loraProgress = 0; + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetLoraProgress); } } - else + catch (Exception ex) { - loraProgress = 0; - try - { - await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetLoraProgress); - } - catch (Exception ex) - { - loraProgress = 1; - throw ex; - } + modelProgress = preModelProgress; + loraProgress = preLoraProgress; + LLMUnitySetup.LogError($"Error downloading the model from URL '{url}': " + ex.Message); + return null; } return AddEntry(modelPath, lora, name, url); } @@ -199,14 +195,14 @@ public static void SetLoraProgress(float progress) public static void Save() { Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); - File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnBuild = downloadOnBuild }, true)); + File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true)); } public static void Load() { if (!File.Exists(LLMUnitySetup.modelListPath)) return; LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)); - downloadOnBuild = store.downloadOnBuild; + downloadOnStart = store.downloadOnStart; modelEntries = store.modelEntries; } } From 9473ea07e61e0b517f4366fd4bd4de52829b9138 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 25 Jul 2024 14:39:49 +0300 Subject: [PATCH 041/105] add label field, register LLM to LLMManager and remove model if not there anymore --- Runtime/LLM.cs | 30 ++++++++++---- Runtime/LLMManager.cs | 96 ++++++++++++++++++++++++++++--------------- 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 87d48fa2..e2ce0b0f 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -80,11 +80,17 @@ public class LLM : MonoBehaviour /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; - /// \cond HIDE - public LLMManager llmManager = new LLMManager(); - public string lora = ""; + /// the LLM model to use. + /// Models with .gguf format are allowed. public string model = ""; + /// Chat template used for the model public string chatTemplate = ChatTemplate.DefaultTemplate; + /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). + /// Models with .bin format are allowed. + public string lora = ""; + + /// \cond HIDE + public LLMManager llmManager = new LLMManager(); IntPtr LLMObject = IntPtr.Zero; List clients = new List(); @@ -95,6 +101,14 @@ public class LLM : MonoBehaviour /// \endcond +#if UNITY_EDITOR + public LLM() + { + LLMManager.Register(this); + } + +#endif + public async Task WaitUntilReady() { while (!started) await Task.Yield(); @@ -109,13 +123,10 @@ public async Task WaitUntilReady() public void SetModel(string path) { // set the model and enable the model editor properties + model = path; #if UNITY_EDITOR - ModelEntry entry = LLMManager.Get(LLMManager.LoadModel(path)); - model = entry.localPath; - SetTemplate(entry.chatTemplate); + SetTemplate(LLMManager.Get(path).chatTemplate); if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); -#else - model = path; #endif } @@ -486,6 +497,9 @@ public void Destroy() public void OnDestroy() { Destroy(); +#if UNITY_EDITOR + LLMManager.Unregister(this); +#endif } } } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index d1469ab2..04e08d8b 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -11,11 +11,12 @@ namespace LLMUnity [Serializable] public class ModelEntry { - public string name; + public string label; + public string filename; + public string path; public bool lora; public string chatTemplate; public string url; - public string localPath; public bool includeInBuild; } @@ -38,6 +39,7 @@ public class LLMManager [HideInInspector] public static float modelProgress = 1; [HideInInspector] public static float loraProgress = 1; + static List llms = new List(); [InitializeOnLoadMethod] static void InitializeOnLoad() @@ -45,20 +47,15 @@ static void InitializeOnLoad() Load(); } - public static string ModelPathToName(string path) + public static string AddEntry(string path, bool lora = false, string label = null, string url = null) { - return Path.GetFileNameWithoutExtension(path.Split("?")[0]); - } - - public static string AddEntry(string path, bool lora = false, string name = null, string url = null) - { - string key = name == null ? ModelPathToName(path) : name; ModelEntry entry = new ModelEntry(); - entry.name = key; + entry.filename = Path.GetFileName(path.Split("?")[0]); + entry.label = label == null ? entry.filename : label; entry.lora = lora; entry.chatTemplate = lora ? null : ChatTemplate.FromGGUF(path); entry.url = url; - entry.localPath = Path.GetFullPath(path).Replace('\\', '/'); + entry.path = Path.GetFullPath(path).Replace('\\', '/'); entry.includeInBuild = true; int indexToInsert = modelEntries.Count; if (!lora) @@ -73,7 +70,7 @@ public static string AddEntry(string path, bool lora = false, string name = null } } modelEntries.Insert(indexToInsert, entry); - return key; + return entry.filename; } public static async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) @@ -85,11 +82,11 @@ public static async Task WaitUntilModelsDownloaded(Callback modelProgress if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); } - public static async Task Download(string url, bool lora = false, string name = null) + public static async Task Download(string url, bool lora = false, string label = null) { foreach (ModelEntry entry in modelEntries) { - if (entry.url == url) return entry.name; + if (entry.url == url) return entry.filename; } string modelName = Path.GetFileName(url).Split("?")[0]; string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); @@ -115,44 +112,44 @@ public static async Task Download(string url, bool lora = false, string LLMUnitySetup.LogError($"Error downloading the model from URL '{url}': " + ex.Message); return null; } - return AddEntry(modelPath, lora, name, url); + return AddEntry(modelPath, lora, label, url); } - public static string Load(string path, bool lora = false, string name = null) + public static string Load(string path, bool lora = false, string label = null) { string fullPath = Path.GetFullPath(path).Replace('\\', '/'); foreach (ModelEntry entry in modelEntries) { - if (entry.localPath == fullPath) return entry.name; + if (entry.path == fullPath) return entry.filename; } - return AddEntry(path, lora, name); + return AddEntry(fullPath, lora, label); } - public static async Task DownloadModel(string url, string name = null) + public static async Task DownloadModel(string url, string label = null) { - return await Download(url, false, name); + return await Download(url, false, label); } - public static async Task DownloadLora(string url, string name = null) + public static async Task DownloadLora(string url, string label = null) { - return await Download(url, true, name); + return await Download(url, true, label); } - public static string LoadModel(string url, string name = null) + public static string LoadModel(string url, string label = null) { - return Load(url, false, name); + return Load(url, false, label); } - public static string LoadLora(string url, string name = null) + public static string LoadLora(string url, string label = null) { - return Load(url, true, name); + return Load(url, true, label); } - public static void SetModelTemplate(string name, string chatTemplate) + public static void SetModelTemplate(string filename, string chatTemplate) { foreach (ModelEntry entry in modelEntries) { - if (entry.name == name) + if (entry.filename == filename) { entry.chatTemplate = chatTemplate; break; @@ -160,24 +157,59 @@ public static void SetModelTemplate(string name, string chatTemplate) } } - public static ModelEntry Get(string name) + public static ModelEntry Get(string filename) { foreach (ModelEntry entry in modelEntries) { - if (entry.name == name) return entry; + if (entry.filename == filename) return entry; } return null; } - public static void Remove(string name) + public static void Remove(string filename) { - Remove(Get(name)); + Remove(Get(filename)); } public static void Remove(ModelEntry entry) { if (entry == null) return; modelEntries.Remove(entry); + foreach (LLM llm in llms) + { + if (!entry.lora && llm.model == entry.filename) llm.model = ""; + else if (entry.lora && llm.lora == entry.filename) llm.lora = ""; + } + } + + public static int Num(bool lora) + { + int num = 0; + foreach (ModelEntry entry in modelEntries) + { + if (entry.lora == lora) num++; + } + return num; + } + + public static int NumModels() + { + return Num(false); + } + + public static int NumLoras() + { + return Num(true); + } + + public static void Register(LLM llm) + { + llms.Add(llm); + } + + public static void Unregister(LLM llm) + { + llms.Remove(llm); } public static void SetModelProgress(float progress) From 2a17b8ad039f595c956f07031ba9478c61a8984c Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 25 Jul 2024 14:40:56 +0300 Subject: [PATCH 042/105] expand button, button improvements, set model/lora if not in LLM --- Editor/LLMEditor.cs | 112 ++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 3a160d70..c78cef3f 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -27,6 +27,7 @@ public class LLMEditor : PropertyEditor string customURL = ""; bool customURLLora = false; bool customURLFocus = false; + bool expandedView = false; protected override Type[] GetPropertyTypes() { @@ -42,19 +43,27 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) { - if (LLMManager.modelEntries.Count == 0) + if (LLMManager.modelEntries.Count > 0) { - DrawFooter(EditorGUILayout.GetControlRect()); - } - else - { - float[] widths = GetColumnWidths(llmScript.advancedOptions); - float listWidth = 2 * ReorderableList.Defaults.padding * 2; + float[] widths = GetColumnWidths(expandedView); + float listWidth = 2 * ReorderableList.Defaults.padding; foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); + EditorGUILayout.BeginHorizontal(GUILayout.Width(listWidth + actionColumnWidth)); + EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); modelList.DoLayoutList(); EditorGUILayout.EndVertical(); + + Rect expandedRect = GUILayoutUtility.GetRect(actionColumnWidth, modelList.elementHeight + ReorderableList.Defaults.padding); + expandedRect.y += modelList.GetHeight() - modelList.elementHeight - ReorderableList.Defaults.padding; + if (GUI.Button(expandedRect, expandedView ? "«" : "»")) + { + expandedView = !expandedView; + Repaint(); + } + EditorGUILayout.EndHorizontal(); } + AddLoadButtons(); bool downloadOnStart = EditorGUILayout.Toggle("Download on Start", LLMManager.downloadOnStart); if (downloadOnStart != LLMManager.downloadOnStart) { @@ -101,7 +110,7 @@ float[] GetColumnWidths(bool expandedView) return widths.ToArray(); } - List CreateColumnRects(Rect rect, bool expandedView) + List CreateColumnRects(Rect rect) { float[] widths = GetColumnWidths(expandedView); float offsetX = rect.x; @@ -131,13 +140,15 @@ void showCustomURLField(bool lora) Repaint(); } - void SetModelIfNone() + void SetModelIfNone(string filename, bool lora) { LLM llmScript = (LLM)target; - if (llmScript.model == "" && LLMManager.modelEntries.Count == 1) llmScript.SetModel(LLMManager.modelEntries[0].localPath); + int num = LLMManager.Num(lora); + if (!lora && llmScript.model == "" && num == 1) llmScript.SetModel(filename); + if (lora && llmScript.lora == "" && num == 1) llmScript.SetLora(filename); } - async Task createCustomURLField(Rect rect) + async Task createCustomURLField() { bool submit = false; bool exit = false; @@ -154,16 +165,13 @@ async Task createCustomURLField(Rect rect) } else { - Rect labelRect = new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight); - Rect textRect = new Rect(rect.x + labelRect.width + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect submitRect = new Rect(rect.x + labelRect.width + buttonWidth + elementPadding * 2, rect.y, buttonWidth / 2f, EditorGUIUtility.singleLineHeight); - Rect backRect = new Rect(rect.x + labelRect.width + buttonWidth * 1.5f + elementPadding * 3, rect.y, buttonWidth / 2f, EditorGUIUtility.singleLineHeight); - - EditorGUI.LabelField(labelRect, "Enter URL:"); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Enter URL", GUILayout.Width(100)); GUI.SetNextControlName("customURLFocus"); - customURL = EditorGUI.TextField(textRect, customURL); - submit = GUI.Button(submitRect, "Submit"); - exit = GUI.Button(backRect, "Back"); + customURL = EditorGUILayout.TextField(customURL, GUILayout.Width(buttonWidth)); + submit = GUILayout.Button("Submit", GUILayout.Width(buttonWidth)); + exit = GUILayout.Button("Back", GUILayout.Width(buttonWidth)); + EditorGUILayout.EndHorizontal(); if (customURLFocus) { @@ -179,32 +187,33 @@ async Task createCustomURLField(Rect rect) Repaint(); if (submit && customURL != "") { - await LLMManager.Download(customURL, customURLLora); - SetModelIfNone(); + string filename = await LLMManager.Download(customURL, customURLLora); + SetModelIfNone(filename, customURLLora); UpdateModels(true); } } } - async Task createButtons(Rect rect, LLM llmScript) + async Task createButtons() { - Rect downloadModelRect = new Rect(rect.x, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect loadModelRect = new Rect(rect.x + buttonWidth + elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect downloadLoraRect = new Rect(rect.xMax - 2 * buttonWidth - elementPadding, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - Rect loadLoraRect = new Rect(rect.xMax - buttonWidth, rect.y, buttonWidth, EditorGUIUtility.singleLineHeight); - int modelIndex = EditorGUI.Popup(downloadModelRect, 0, modelOptions.ToArray()); + LLM llmScript = (LLM)target; + EditorGUILayout.BeginHorizontal(); + + GUIStyle centeredPopupStyle = new GUIStyle(EditorStyles.popup); + centeredPopupStyle.alignment = TextAnchor.MiddleCenter; + int modelIndex = EditorGUILayout.Popup(0, modelOptions.ToArray(), centeredPopupStyle, GUILayout.Width(buttonWidth)); if (modelIndex == 1) { showCustomURLField(false); } else if (modelIndex > 1) { - await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); - SetModelIfNone(); + string filename = await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); + SetModelIfNone(filename, false); UpdateModels(true); } - if (GUI.Button(loadModelRect, "Load model")) + if (GUILayout.Button("Load model", GUILayout.Width(buttonWidth))) { EditorApplication.delayCall += () => { @@ -216,14 +225,16 @@ async Task createButtons(Rect rect, LLM llmScript) } }; } + EditorGUILayout.EndHorizontal(); if (llmScript.advancedOptions) { - if (GUI.Button(downloadLoraRect, "Download LoRA")) + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Download LoRA", GUILayout.Width(buttonWidth))) { showCustomURLField(true); } - if (GUI.Button(loadLoraRect, "Load LoRA")) + if (GUILayout.Button("Load LoRA", GUILayout.Width(buttonWidth))) { EditorApplication.delayCall += () => { @@ -234,14 +245,14 @@ async Task createButtons(Rect rect, LLM llmScript) } }; } + EditorGUILayout.EndHorizontal(); } } - async void DrawFooter(Rect rect) + async Task AddLoadButtons() { - LLM llmScript = (LLM)target; - if (showCustomURL) await createCustomURLField(rect); - else await createButtons(rect, llmScript); + if (showCustomURL) await createCustomURLField(); + else await createButtons(); } void OnEnable() @@ -261,14 +272,14 @@ void OnEnable() if (index >= LLMManager.modelEntries.Count) return; ModelEntry entry = LLMManager.modelEntries[index]; - List rects = CreateColumnRects(rect, llmScript.advancedOptions); + List rects = CreateColumnRects(rect); int col = 0; Rect selectRect = rects[col++]; Rect nameRect = rects[col++]; Rect templateRect = rects[col++]; Rect urlRect = new Rect(); Rect pathRect = new Rect(); - if (llmScript.advancedOptions) + if (expandedView) { urlRect = rects[col++]; pathRect = rects[col++]; @@ -276,25 +287,25 @@ void OnEnable() Rect includeInBuildRect = rects[col++]; Rect actionRect = rects[col++]; - bool hasPath = entry.localPath != null && entry.localPath != ""; + bool hasPath = entry.path != null && entry.path != ""; bool hasURL = entry.url != null && entry.url != ""; bool isSelected = false; if (!entry.lora) { - isSelected = llmScript.model == entry.localPath; + isSelected = llmScript.model == entry.filename; bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); - if (newSelected && !isSelected) llmScript.SetModel(entry.localPath); + if (newSelected && !isSelected) llmScript.SetModel(entry.filename); } else { - isSelected = llmScript.lora == entry.localPath; + isSelected = llmScript.lora == entry.filename; bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); - if (newSelected && !isSelected) llmScript.SetLora(entry.localPath); + if (newSelected && !isSelected) llmScript.SetLora(entry.filename); else if (!newSelected && isSelected) llmScript.SetLora(""); } - DrawCopyableLabel(nameRect, entry.name); + DrawCopyableLabel(nameRect, entry.label); if (!entry.lora) { @@ -308,7 +319,7 @@ void OnEnable() } } - if (llmScript.advancedOptions) + if (expandedView) { if (hasURL) { @@ -323,7 +334,7 @@ void OnEnable() UpdateModels(); } } - DrawCopyableLabel(pathRect, entry.localPath); + DrawCopyableLabel(pathRect, entry.path); } bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); @@ -346,12 +357,12 @@ void OnEnable() }, drawHeaderCallback = (rect) => { - List rects = CreateColumnRects(rect, llmScript.advancedOptions); + List rects = CreateColumnRects(rect); int col = 0; EditorGUI.LabelField(rects[col++], ""); EditorGUI.LabelField(rects[col++], "Model"); EditorGUI.LabelField(rects[col++], "Chat template"); - if (llmScript.advancedOptions) + if (expandedView) { EditorGUI.LabelField(rects[col++], "URL"); EditorGUI.LabelField(rects[col++], "Path"); @@ -359,7 +370,8 @@ void OnEnable() EditorGUI.LabelField(rects[col++], "Build"); EditorGUI.LabelField(rects[col++], ""); }, - drawFooterCallback = DrawFooter, + drawFooterCallback = {}, + footerHeight = 0, }; } From 04b0c5093f20c6c360daa0a795b753f97593b6c1 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 25 Jul 2024 14:49:10 +0300 Subject: [PATCH 043/105] simplify AddAsset --- Runtime/LLMCharacter.cs | 2 +- Runtime/LLMUnitySetup.cs | 25 +++++++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 7fb45489..cd3a192c 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -322,7 +322,7 @@ public async Task LoadTemplate() public void SetGrammar(string path) { #if UNITY_EDITOR - if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); + if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path); #endif grammar = path; InitGrammar(); diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 2ebce5a7..7e8cd498 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -288,30 +288,23 @@ private static void SetLibraryProgress(float progress) libraryProgress = progress; } - public static string AddAsset(string assetPath, string basePath) + public static string AddAsset(string assetPath) { if (!File.Exists(assetPath)) { LogError($"{assetPath} does not exist!"); return null; } - // add an asset to the basePath directory if it is not already there and return the relative path - string basePathSlash = basePath.Replace('\\', '/'); - string fullPath = Path.GetFullPath(assetPath).Replace('\\', '/'); - Directory.CreateDirectory(basePathSlash); - if (!fullPath.StartsWith(basePathSlash)) + string filename = Path.GetFileName(assetPath); + string fullPath = GetAssetPath(filename); + AssetDatabase.StartAssetEditing(); + foreach (string path in new string[] {fullPath, fullPath + ".meta"}) { - // if the asset is not in the assets dir copy it over - fullPath = Path.Combine(basePathSlash, Path.GetFileName(assetPath)); - AssetDatabase.StartAssetEditing(); - foreach (string filename in new string[] {fullPath, fullPath + ".meta"}) - { - if (File.Exists(filename)) File.Delete(filename); - } - CreateSymlink(assetPath, fullPath); - AssetDatabase.StopAssetEditing(); + if (File.Exists(path)) File.Delete(path); } - return fullPath.Substring(basePathSlash.Length + 1); + File.Copy(assetPath, fullPath); + AssetDatabase.StopAssetEditing(); + return filename; } public static void CreateSymlink(string sourcePath, string targetPath) From 54554eda07ae5b6de81e3fba234f3546ffa5970b Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 25 Jul 2024 15:00:51 +0300 Subject: [PATCH 044/105] use LLM manager on Editor mode otherwise GetAssetPath --- Runtime/LLM.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index e2ce0b0f..6db36a56 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -90,7 +90,6 @@ public class LLM : MonoBehaviour public string lora = ""; /// \cond HIDE - public LLMManager llmManager = new LLMManager(); IntPtr LLMObject = IntPtr.Zero; List clients = new List(); @@ -102,6 +101,9 @@ public class LLM : MonoBehaviour /// \endcond #if UNITY_EDITOR + + public LLMManager llmManager = new LLMManager(); + public LLM() { LLMManager.Register(this); @@ -171,7 +173,11 @@ protected virtual string GetLlamaccpArguments() LLMUnitySetup.LogError("No model file provided!"); return null; } +#if UNITY_EDITOR + string modelPath = LLMManager.Get(model).path; +#else string modelPath = LLMUnitySetup.GetAssetPath(model); +#endif if (!File.Exists(modelPath)) { LLMUnitySetup.LogError($"File {modelPath} not found!"); @@ -180,7 +186,11 @@ protected virtual string GetLlamaccpArguments() string loraPath = ""; if (lora != "") { +#if UNITY_EDITOR + loraPath = LLMManager.Get(lora).path; +#else loraPath = LLMUnitySetup.GetAssetPath(lora); +#endif if (!File.Exists(loraPath)) { LLMUnitySetup.LogError($"File {loraPath} not found!"); From 936a35ee8c6ba5b5045c259664d621d6b639a881 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 13:15:09 +0300 Subject: [PATCH 045/105] improve build process --- Editor/LLMBuildProcessor.cs | 142 +++++++----------------------------- Editor/LLMEditor.cs | 6 +- Runtime/LLMBuilder.cs | 120 ++++++++++++++++++++++++++++++ Runtime/LLMBuilder.cs.meta | 11 +++ Runtime/LLMUnitySetup.cs | 78 +++++++++++++------- 5 files changed, 209 insertions(+), 148 deletions(-) create mode 100644 Runtime/LLMBuilder.cs create mode 100644 Runtime/LLMBuilder.cs.meta diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index 05cc6238..b98caf0f 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -2,150 +2,58 @@ using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; -using System.IO; -using System.Collections.Generic; -using System; namespace LLMUnity { - public class LLMBuildProcessor : MonoBehaviour, IPreprocessBuildWithReport, IPostprocessBuildWithReport + public class LLMBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport { public int callbackOrder => 0; - static string tempDir = Path.Combine(Application.temporaryCachePath, "LLMBuildProcessor", Path.GetFileName(LLMUnitySetup.libraryPath)); - static List movedPairs = new List(); - static string movedCache = Path.Combine(tempDir, "moved.json"); - [InitializeOnLoadMethod] - private static void InitializeOnLoad() - { - if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir); - else ResetMoves(); - } - - // CALLED BEFORE THE BUILD + // called before the build public void OnPreprocessBuild(BuildReport report) { - // Start listening for errors when build starts Application.logMessageReceived += OnBuildError; - HideLibraryPlatforms(report.summary.platform); - HideModels(); - if (movedPairs.Count > 0) AssetDatabase.Refresh(); - } - - // CALLED DURING BUILD TO CHECK FOR ERRORS - private void OnBuildError(string condition, string stacktrace, LogType type) - { - if (type == LogType.Error) - { - // FAILED TO BUILD, STOP LISTENING FOR ERRORS - BuildCompleted(); - } - } - - // CALLED AFTER THE BUILD - public void OnPostprocessBuild(BuildReport report) - { - BuildCompleted(); - } - - public void BuildCompleted() - { - Application.logMessageReceived -= OnBuildError; - ResetMoves(); - } - - static bool MovePath(string source, string target) - { - bool moved = false; - if (File.Exists(source)) - { - File.Move(source, target); - moved = true; - } - else if (Directory.Exists(source)) - { - Directory.Move(source, target); - moved = true; - } - if (moved) - { - movedPairs.Add(new MovedPair {source = source, target = target}); - File.WriteAllText(movedCache, JsonUtility.ToJson(new FoldersMovedWrapper { movedPairs = movedPairs })); - } - return moved; - } - - static void MoveAssetAndMeta(string source, string target) - { - MovePath(source + ".meta", target + ".meta"); - MovePath(source, target); - } - - static void HideLibraryPlatforms(BuildTarget buildPlatform) - { - List platforms = new List(){ "windows", "macos", "linux", "android" }; - switch (buildPlatform) + string platform = null; + switch (report.summary.platform) { case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: - platforms.Remove("windows"); + platform = "windows"; break; case BuildTarget.StandaloneLinux64: - platforms.Remove("linux"); + platform = "linux"; break; case BuildTarget.StandaloneOSX: - platforms.Remove("macos"); + platform = "macos"; break; case BuildTarget.Android: - platforms.Remove("android"); + platform = "android"; + break; + case BuildTarget.iOS: + platform = "ios"; break; } - - foreach (string dirname in Directory.GetDirectories(LLMUnitySetup.libraryPath)) - { - foreach (string platform in platforms) - { - if (Path.GetFileName(dirname).StartsWith(platform)) - { - MoveAssetAndMeta(dirname, Path.Combine(tempDir, Path.GetFileName(dirname))); - } - } - } + LLMBuilder.HideLibraryPlatforms(platform); + LLMBuilder.CopyModels(); + AssetDatabase.Refresh(); } - static void HideModels() + // called during build to check for errors + private void OnBuildError(string condition, string stacktrace, LogType type) { - foreach (LLM llm in FindObjectsOfType()) - { - // if (!llm.downloadOnBuild) continue; - // if (llm.modelURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.model), Path.Combine(tempDir, Path.GetFileName(llm.model))); - if (llm.loraURL != "") MoveAssetAndMeta(LLMUnitySetup.GetAssetPath(llm.lora), Path.Combine(tempDir, Path.GetFileName(llm.lora))); - } + if (type == LogType.Error) BuildCompleted(); } - static void ResetMoves() + // called after the build + public void OnPostprocessBuild(BuildReport report) { - if (!File.Exists(movedCache)) return; - List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).movedPairs; - if (movedPairs == null) return; - - bool refresh = false; - foreach (var pair in movedPairs) refresh |= MovePath(pair.target, pair.source); - if (refresh) AssetDatabase.Refresh(); - File.Delete(movedCache); + BuildCompleted(); } - } - [Serializable] - public struct MovedPair - { - public string source; - public string target; - } - - [Serializable] - public class FoldersMovedWrapper - { - public List movedPairs; + public void BuildCompleted() + { + Application.logMessageReceived -= OnBuildError; + LLMBuilder.Reset(); + } } } diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index c78cef3f..cb482931 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -63,7 +63,7 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) } EditorGUILayout.EndHorizontal(); } - AddLoadButtons(); + _ = AddLoadButtons(); bool downloadOnStart = EditorGUILayout.Toggle("Download on Start", LLMManager.downloadOnStart); if (downloadOnStart != LLMManager.downloadOnStart) { @@ -92,8 +92,8 @@ static void ResetModelOptions() { List existingOptions = new List(); foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); - modelOptions = new List(); - modelURLs = new List(); + modelOptions = new List(){"Download model", "Custom URL"}; + modelURLs = new List(){null, null}; foreach ((string name, string url) in LLMUnitySetup.modelOptions) { if (url != null && existingOptions.Contains(url)) continue; diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs new file mode 100644 index 00000000..ff433961 --- /dev/null +++ b/Runtime/LLMBuilder.cs @@ -0,0 +1,120 @@ +using UnityEditor; +using UnityEngine; +using System.IO; +using System.Collections.Generic; +using System; + +#if UNITY_EDITOR +namespace LLMUnity +{ + public class LLMBuilder + { + static List movedPairs = new List(); + static string movedCache = Path.Combine(LLMUnitySetup.buildTempDir, "moved.json"); + + [InitializeOnLoadMethod] + private static void InitializeOnLoad() + { + Directory.CreateDirectory(LLMUnitySetup.buildTempDir); + Reset(); + } + + public delegate void ActionCallback(string source, string target); + + static void AddMovedPair(string source, string target) + { + movedPairs.Add(new MovedPair {source = source, target = target}); + File.WriteAllText(movedCache, JsonUtility.ToJson(new FoldersMovedWrapper { movedPairs = movedPairs }, true)); + } + + static bool MoveAction(string source, string target, bool addEntry = true) + { + ActionCallback moveCallback; + if (File.Exists(source)) moveCallback = File.Move; + else if (Directory.Exists(source)) moveCallback = LLMUnitySetup.MovePath; + else return false; + + if (addEntry) AddMovedPair(source, target); + moveCallback(source, target); + return true; + } + + static bool CopyAction(string source, string target, bool addEntry = true) + { + ActionCallback copyCallback; + if (File.Exists(source)) copyCallback = File.Copy; + else if (Directory.Exists(source)) copyCallback = LLMUnitySetup.CopyPath; + else return false; + + if (addEntry) AddMovedPair("", target); + copyCallback(source, target); + return true; + } + + static bool DeleteAction(string source) + { + return LLMUnitySetup.DeletePath(source); + } + + public static void HideLibraryPlatforms(string platform) + { + List platforms = new List(){ "windows", "macos", "linux", "android", "ios" }; + platforms.Remove(platform); + foreach (string source in Directory.GetDirectories(LLMUnitySetup.libraryPath)) + { + foreach (string platformPrefix in platforms) + { + if (Path.GetFileName(source).StartsWith(platformPrefix)) + { + string target = Path.Combine(LLMUnitySetup.buildTempDir, Path.GetFileName(source)); + MoveAction(source, target); + MoveAction(source + ".meta", target + ".meta"); + } + } + } + } + + public static void CopyModels() + { + if (LLMManager.downloadOnStart) return; + foreach (ModelEntry modelEntry in LLMManager.modelEntries) + { + string source = modelEntry.path; + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (!modelEntry.includeInBuild || File.Exists(target)) continue; + CopyAction(source, target); + AddMovedPair("", target + ".meta"); + } + } + + public static void Reset() + { + if (!File.Exists(movedCache)) return; + List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).movedPairs; + if (movedPairs == null) return; + + bool refresh = false; + foreach (var pair in movedPairs) + { + if (pair.source == "") refresh |= DeleteAction(pair.target); + else refresh |= MoveAction(pair.target, pair.source, false); + } + if (refresh) AssetDatabase.Refresh(); + LLMUnitySetup.DeletePath(movedCache); + } + } + + [Serializable] + public struct MovedPair + { + public string source; + public string target; + } + + [Serializable] + public class FoldersMovedWrapper + { + public List movedPairs; + } +} +#endif diff --git a/Runtime/LLMBuilder.cs.meta b/Runtime/LLMBuilder.cs.meta new file mode 100644 index 00000000..14615c3e --- /dev/null +++ b/Runtime/LLMBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e52304e7914527ae0801d752670d7bec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 7e8cd498..993e6942 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -78,16 +78,18 @@ public class LLMUnitySetup public static string LlamaLibURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}/undreamai-{LlamaLibVersion}-llamacpp.zip"; /// LlamaLib path public static string libraryPath = GetAssetPath(Path.GetFileName(LlamaLibURL).Replace(".zip", "")); + /// LLMnity store path + public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); /// Model download path - public static string modelDownloadPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); + public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); /// Model list for project public static string modelListPath = Path.Combine(Application.temporaryCachePath, "modelCache.json"); + /// Temporary dir for build + public static string buildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); /// Default models for download [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] { - ("Download model", null), - ("Custom URL", null), ("Mistral 7B Instruct v0.2 (medium, best overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), ("OpenHermes 2.5 7B (medium, best for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), ("Phi 3 (small, great)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), @@ -228,11 +230,11 @@ public static async Task AndroidExtractFile(string assetName, bool overwrite = f string target = GetAssetPath(assetName); if (!overwrite && File.Exists(target)) { - Debug.Log($"File {target} already exists"); + Log($"File {target} already exists"); return; } - Debug.Log($"Extracting {source} to {target}"); + Log($"Extracting {source} to {target}"); // UnityWebRequest to read the file from StreamingAssets UnityWebRequest www = UnityWebRequest.Get(source); @@ -242,7 +244,7 @@ public static async Task AndroidExtractFile(string assetName, bool overwrite = f while (!operation.isDone) await Task.Delay(1); if (www.result != UnityWebRequest.Result.Success) { - Debug.LogError("Failed to load file from StreamingAssets: " + www.error); + LogError("Failed to load file from StreamingAssets: " + www.error); } else { @@ -267,6 +269,44 @@ public static bool IsSubPath(string childPath, string parentPath) } #if UNITY_EDITOR + + public static void CopyPath(string source, string target) + { + if (File.Exists(source)) + { + File.Copy(source, target); + } + else if (Directory.Exists(source)) + { + Directory.CreateDirectory(target); + List filesAndDirs = new List(); + filesAndDirs.AddRange(Directory.GetFiles(source)); + filesAndDirs.AddRange(Directory.GetDirectories(source)); + foreach (string path in filesAndDirs) + { + CopyPath(path, Path.Combine(target, Path.GetFileName(path))); + } + } + } + + public static void MovePath(string source, string target) + { + CopyPath(source, target); + DeletePath(source); + } + + public static bool DeletePath(string path) + { + if (!IsSubPath(path, GetAssetPath()) && !IsSubPath(path, buildTempDir)) + { + LogError($"Safeguard: {path} will not be deleted because it may not be safe"); + return false; + } + if (File.Exists(path)) File.Delete(path); + else if (Directory.Exists(path)) Directory.Delete(path, true); + return true; + } + [HideInInspector] public static float libraryProgress = 1; private static async Task DownloadLibrary() @@ -277,7 +317,9 @@ private static async Task DownloadLibrary() if (!Directory.Exists(libraryPath)) { await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); + AssetDatabase.StartAssetEditing(); ZipFile.ExtractToDirectory(libZip, libraryPath); + AssetDatabase.StopAssetEditing(); File.Delete(libZip); } libraryProgress = 1; @@ -307,26 +349,6 @@ public static string AddAsset(string assetPath) return filename; } - public static void CreateSymlink(string sourcePath, string targetPath) - { - bool isDirectory = Directory.Exists(sourcePath); - if (!isDirectory && !File.Exists(sourcePath)) throw new FileNotFoundException($"Source path does not exist: {sourcePath}"); - - bool success; -#if UNITY_STANDALONE_WIN - success = CreateSymbolicLink(targetPath, sourcePath, (int)isDirectory); -#else - success = symlink(sourcePath, targetPath) == 0; -#endif - if (!success) throw new IOException($"Failed to create symbolic link: {targetPath}"); - } - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); - - [DllImport("libc", SetLastError = true)] - private static extern int symlink(string oldpath, string newpath); - #endif /// \endcond public static int GetMaxFreqKHz(int cpuId) @@ -422,7 +444,7 @@ public static int AndroidGetNumBigCores() } catch (Exception e) { - Debug.LogError(e.Message); + LogError(e.Message); } int numBigCores = 0; @@ -474,7 +496,7 @@ public static int AndroidGetNumBigCoresCapacity() } catch (Exception e) { - Debug.LogError(e.Message); + LogError(e.Message); } int numBigCores = 0; From 23c1eb3ea9e2edc6fa22a2b977d1f6fd064c3e6a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 13:43:14 +0300 Subject: [PATCH 046/105] store models in a player pref --- Runtime/LLMManager.cs | 13 +++++++++---- Runtime/LLMUnitySetup.cs | 3 --- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 04e08d8b..b5d05fac 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -29,6 +29,7 @@ public class LLMManagerStore public class LLMManager { + static string LLMManagerPref = "LLMManager"; public static bool downloadOnStart = false; public static List modelEntries = new List(); @@ -70,6 +71,7 @@ public static string AddEntry(string path, bool lora = false, string label = nul } } modelEntries.Insert(indexToInsert, entry); + Save(); return entry.filename; } @@ -175,6 +177,7 @@ public static void Remove(ModelEntry entry) { if (entry == null) return; modelEntries.Remove(entry); + Save(); foreach (LLM llm in llms) { if (!entry.lora && llm.model == entry.filename) llm.model = ""; @@ -226,14 +229,16 @@ public static void SetLoraProgress(float progress) public static void Save() { - Directory.CreateDirectory(Path.GetDirectoryName(LLMUnitySetup.modelListPath)); - File.WriteAllText(LLMUnitySetup.modelListPath, JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true)); + string pref = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true); + PlayerPrefs.SetString(LLMManagerPref, pref); + PlayerPrefs.Save(); } public static void Load() { - if (!File.Exists(LLMUnitySetup.modelListPath)) return; - LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.modelListPath)); + string pref = PlayerPrefs.GetString(LLMManagerPref); + if (pref == null || pref == "") return; + LLMManagerStore store = JsonUtility.FromJson(pref); downloadOnStart = store.downloadOnStart; modelEntries = store.modelEntries; } diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 993e6942..e6e70354 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -7,7 +7,6 @@ using System; using System.IO.Compression; using System.Collections.Generic; -using System.Runtime.InteropServices; using UnityEngine.Networking; /// @defgroup llm LLM @@ -82,8 +81,6 @@ public class LLMUnitySetup public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); /// Model download path public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); - /// Model list for project - public static string modelListPath = Path.Combine(Application.temporaryCachePath, "modelCache.json"); /// Temporary dir for build public static string buildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); From 4e5afb1634f38fab58cc072b655b931990a04f97 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 17:13:15 +0300 Subject: [PATCH 047/105] download models with LLMManager --- Editor/LLMBuildProcessor.cs | 2 +- Editor/LLMEditor.cs | 16 ++- Runtime/LLM.cs | 46 +++++---- Runtime/LLMBuilder.cs | 57 +++++------ Runtime/LLMManager.cs | 150 +++++++++++++++++++++++----- Runtime/LLMUnitySetup.cs | 20 +++- Runtime/ResumingWebClient.cs | 9 +- Samples~/AndroidDemo/AndroidDemo.cs | 4 +- 8 files changed, 210 insertions(+), 94 deletions(-) diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index b98caf0f..23165cbf 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -34,7 +34,7 @@ public void OnPreprocessBuild(BuildReport report) break; } LLMBuilder.HideLibraryPlatforms(platform); - LLMBuilder.CopyModels(); + LLMBuilder.BuildModels(); AssetDatabase.Refresh(); } diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index cb482931..b9354cfa 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -21,7 +21,6 @@ public class LLMEditor : PropertyEditor static GUIContent trashIcon; static List modelOptions; static List modelURLs; - string[] templateOptions; string elementFocus = ""; bool showCustomURL = false; string customURL = ""; @@ -126,7 +125,6 @@ List CreateColumnRects(Rect rect) void UpdateModels(bool resetOptions = false) { - LLMManager.Save(); if (resetOptions) ResetModelOptions(); Repaint(); } @@ -259,7 +257,6 @@ void OnEnable() { LLM llmScript = (LLM)target; ResetModelOptions(); - templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); Texture2D loraLineTexture = new Texture2D(1, 1); loraLineTexture.SetPixel(0, 0, Color.black); @@ -309,12 +306,13 @@ void OnEnable() if (!entry.lora) { - int templateIndex = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), entry.chatTemplate); - int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateOptions); + string[] templateDescriptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); + string[] templates = ChatTemplate.templatesDescription.Values.ToList().ToArray(); + int templateIndex = Array.IndexOf(templates, entry.chatTemplate); + int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateDescriptions); if (newTemplateIndex != templateIndex) { - entry.chatTemplate = ChatTemplate.templatesDescription[templateOptions[newTemplateIndex]]; - if (isSelected) llmScript.SetTemplate(entry.chatTemplate); + LLMManager.SetTemplate(entry.filename, templates[newTemplateIndex]); UpdateModels(); } } @@ -330,7 +328,7 @@ void OnEnable() string newURL = EditorGUI.TextField(urlRect, entry.url); if (newURL != entry.url) { - entry.url = newURL; + LLMManager.SetURL(entry, newURL); UpdateModels(); } } @@ -340,7 +338,7 @@ void OnEnable() bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); if (includeInBuild != entry.includeInBuild) { - entry.includeInBuild = includeInBuild; + LLMManager.SetIncludeInBuild(entry, includeInBuild); UpdateModels(); } diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 6db36a56..4e111541 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -79,6 +79,8 @@ public class LLM : MonoBehaviour public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; + /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. + public static bool modelsDownloaded { get; protected set; } = false; /// the LLM model to use. /// Models with .gguf format are allowed. @@ -100,6 +102,26 @@ public class LLM : MonoBehaviour /// \endcond + /// + /// The Unity Awake function that starts the LLM server. + /// The server can be started asynchronously if the asynchronousStartup option is set. + /// + public async void Awake() + { + if (!enabled) return; +#if !UNITY_EDITOR + await LLMManager.DownloadModels(); +#endif + modelsDownloaded = true; + // if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); + string arguments = GetLlamaccpArguments(); + if (arguments == null) return; + if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); + else StartLLMServer(arguments); + if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); + if (basePrompt != "") await SetBasePrompt(basePrompt); + } + #if UNITY_EDITOR public LLMManager llmManager = new LLMManager(); @@ -116,6 +138,12 @@ public async Task WaitUntilReady() while (!started) await Task.Yield(); } + public static async Task WaitUntilModelsDownloaded(Callback downloadProgressCallback = null) + { + if (downloadProgressCallback != null) LLMManager.downloadProgressCallbacks.Add(downloadProgressCallback); + while (!modelsDownloaded) await Task.Yield(); + } + /// /// Allows to set the model used by the LLM. /// The model provided is copied to the Assets/StreamingAssets folder that allows it to also work in the build. @@ -210,24 +238,6 @@ protected virtual string GetLlamaccpArguments() return arguments; } - /// - /// The Unity Awake function that starts the LLM server. - /// The server can be started asynchronously if the asynchronousStartup option is set. - /// - public async void Awake() - { - if (!enabled) return; - // if (downloadOnBuild) await DownloadModels(); - // modelsDownloaded = true; - // if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); - string arguments = GetLlamaccpArguments(); - if (arguments == null) return; - if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); - else StartLLMServer(arguments); - if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); - if (basePrompt != "") await SetBasePrompt(basePrompt); - } - private void SetupLogging() { logStreamWrapper = ConstructStreamWrapper(LLMUnitySetup.LogWarning, true); diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs index ff433961..db7e80c3 100644 --- a/Runtime/LLMBuilder.cs +++ b/Runtime/LLMBuilder.cs @@ -2,29 +2,31 @@ using UnityEngine; using System.IO; using System.Collections.Generic; -using System; #if UNITY_EDITOR namespace LLMUnity { public class LLMBuilder { - static List movedPairs = new List(); - static string movedCache = Path.Combine(LLMUnitySetup.buildTempDir, "moved.json"); + static List movedPairs = new List(); + static string movedCache = Path.Combine(LLMUnitySetup.BuildTempDir, "moved.json"); [InitializeOnLoadMethod] private static void InitializeOnLoad() { - Directory.CreateDirectory(LLMUnitySetup.buildTempDir); + Directory.CreateDirectory(LLMUnitySetup.BuildTempDir); Reset(); } - public delegate void ActionCallback(string source, string target); - static void AddMovedPair(string source, string target) { - movedPairs.Add(new MovedPair {source = source, target = target}); - File.WriteAllText(movedCache, JsonUtility.ToJson(new FoldersMovedWrapper { movedPairs = movedPairs }, true)); + movedPairs.Add(new StringPair {source = source, target = target}); + File.WriteAllText(movedCache, JsonUtility.ToJson(new ListStringPair { pairs = movedPairs }, true)); + } + + static void AddTargetPair(string target) + { + AddMovedPair("", target); } static bool MoveAction(string source, string target, bool addEntry = true) @@ -46,11 +48,17 @@ static bool CopyAction(string source, string target, bool addEntry = true) else if (Directory.Exists(source)) copyCallback = LLMUnitySetup.CopyPath; else return false; - if (addEntry) AddMovedPair("", target); + if (addEntry) AddTargetPair(target); copyCallback(source, target); return true; } + static void CopyActionAddMeta(string source, string target) + { + CopyAction(source, target); + AddTargetPair(target + ".meta"); + } + static bool DeleteAction(string source) { return LLMUnitySetup.DeletePath(source); @@ -66,7 +74,7 @@ public static void HideLibraryPlatforms(string platform) { if (Path.GetFileName(source).StartsWith(platformPrefix)) { - string target = Path.Combine(LLMUnitySetup.buildTempDir, Path.GetFileName(source)); + string target = Path.Combine(LLMUnitySetup.BuildTempDir, Path.GetFileName(source)); MoveAction(source, target); MoveAction(source + ".meta", target + ".meta"); } @@ -74,23 +82,17 @@ public static void HideLibraryPlatforms(string platform) } } - public static void CopyModels() + public static void BuildModels() { - if (LLMManager.downloadOnStart) return; - foreach (ModelEntry modelEntry in LLMManager.modelEntries) - { - string source = modelEntry.path; - string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); - if (!modelEntry.includeInBuild || File.Exists(target)) continue; - CopyAction(source, target); - AddMovedPair("", target + ".meta"); - } + LLMUnitySetup.DeletePath(LLMUnitySetup.BuildFile); + LLMManager.Build(CopyActionAddMeta); + if (File.Exists(LLMUnitySetup.BuildFile)) AddTargetPair(LLMUnitySetup.BuildFile); } public static void Reset() { if (!File.Exists(movedCache)) return; - List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).movedPairs; + List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).pairs; if (movedPairs == null) return; bool refresh = false; @@ -103,18 +105,5 @@ public static void Reset() LLMUnitySetup.DeletePath(movedCache); } } - - [Serializable] - public struct MovedPair - { - public string source; - public string target; - } - - [Serializable] - public class FoldersMovedWrapper - { - public List movedPairs; - } } #endif diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index b5d05fac..e644d72b 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -1,4 +1,3 @@ -#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; @@ -8,6 +7,7 @@ namespace LLMUnity { +#if UNITY_EDITOR [Serializable] public class ModelEntry { @@ -26,18 +26,78 @@ public class LLMManagerStore public bool downloadOnStart; public List modelEntries; } +#endif public class LLMManager { + public static float downloadProgress = 1; + public static List> downloadProgressCallbacks = new List>(); + static long totalSize; + static long currFileSize; + static long completedSize; + + public static void SetDownloadProgress(float progress) + { + downloadProgress = (completedSize + progress * currFileSize) / totalSize; + foreach (Callback downloadProgressCallback in downloadProgressCallbacks) downloadProgressCallback?.Invoke(downloadProgress); + } + + public static async Task DownloadModels() + { + if (!File.Exists(LLMUnitySetup.BuildFile)) return; + + List downloads = new List(); + using (FileStream fs = new FileStream(LLMUnitySetup.BuildFile, FileMode.Open, FileAccess.Read)) + { + using (BinaryReader reader = new BinaryReader(fs)) + { + List downloadsToDo = JsonUtility.FromJson(reader.ReadString()).pairs; + foreach (StringPair pair in downloadsToDo) + { + string target = LLMUnitySetup.GetAssetPath(pair.target); + if (!File.Exists(target)) downloads.Add(new StringPair {source = pair.source, target = target}); + } + } + } + if (downloads.Count == 0) return; + + try + { + downloadProgress = 0; + totalSize = 0; + completedSize = 0; + + ResumingWebClient client = new ResumingWebClient(); + Dictionary fileSizes = new Dictionary(); + foreach (StringPair pair in downloads) + { + long size = client.GetURLFileSize(pair.source); + fileSizes[pair.source] = size; + totalSize += size; + } + + foreach (StringPair pair in downloads) + { + currFileSize = fileSizes[pair.source]; + await LLMUnitySetup.DownloadFile(pair.source, pair.target, false, null, SetDownloadProgress); + totalSize += currFileSize; + } + + completedSize = totalSize; + SetDownloadProgress(0); + } + catch (Exception ex) + { + LLMUnitySetup.LogError($"Error downloading the models"); + throw ex; + } + } + +#if UNITY_EDITOR static string LLMManagerPref = "LLMManager"; public static bool downloadOnStart = false; public static List modelEntries = new List(); - /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. - public static bool modelsDownloaded { get; protected set; } = false; - static List> modelProgressCallbacks = new List>(); - static List> loraProgressCallbacks = new List>(); - [HideInInspector] public static float modelProgress = 1; [HideInInspector] public static float loraProgress = 1; static List llms = new List(); @@ -75,15 +135,6 @@ public static string AddEntry(string path, bool lora = false, string label = nul return entry.filename; } - public static async Task WaitUntilModelsDownloaded(Callback modelProgressCallback = null, Callback loraProgressCallback = null) - { - if (modelProgressCallback != null) modelProgressCallbacks.Add(modelProgressCallback); - if (loraProgressCallback != null) loraProgressCallbacks.Add(loraProgressCallback); - while (!modelsDownloaded) await Task.Yield(); - if (modelProgressCallback != null) modelProgressCallbacks.Remove(modelProgressCallback); - if (loraProgressCallback != null) loraProgressCallbacks.Remove(loraProgressCallback); - } - public static async Task Download(string url, bool lora = false, string label = null) { foreach (ModelEntry entry in modelEntries) @@ -147,16 +198,44 @@ public static string LoadLora(string url, string label = null) return Load(url, true, label); } - public static void SetModelTemplate(string filename, string chatTemplate) + public static void SetTemplate(string filename, string chatTemplate) { - foreach (ModelEntry entry in modelEntries) + SetTemplate(Get(filename), chatTemplate); + } + + public static void SetTemplate(ModelEntry entry, string chatTemplate) + { + if (entry == null) return; + entry.chatTemplate = chatTemplate; + foreach (LLM llm in llms) { - if (entry.filename == filename) - { - entry.chatTemplate = chatTemplate; - break; - } + if (llm.model == entry.filename) llm.SetTemplate(chatTemplate); } + Save(); + } + + public static void SetURL(string filename, string url) + { + SetURL(Get(filename), url); + } + + public static void SetURL(ModelEntry entry, string url) + { + if (entry == null) return; + entry.url = url; + Save(); + } + + public static void SetIncludeInBuild(string filename, bool includeInBuild) + { + SetIncludeInBuild(Get(filename), includeInBuild); + } + + public static void SetIncludeInBuild(ModelEntry entry, bool includeInBuild) + { + if (entry == null) return; + entry.includeInBuild = includeInBuild; + Save(); } public static ModelEntry Get(string filename) @@ -218,13 +297,11 @@ public static void Unregister(LLM llm) public static void SetModelProgress(float progress) { modelProgress = progress; - foreach (Callback modelProgressCallback in modelProgressCallbacks) modelProgressCallback?.Invoke(progress); } public static void SetLoraProgress(float progress) { loraProgress = progress; - foreach (Callback loraProgressCallback in loraProgressCallbacks) loraProgressCallback?.Invoke(progress); } public static void Save() @@ -242,6 +319,29 @@ public static void Load() downloadOnStart = store.downloadOnStart; modelEntries = store.modelEntries; } + + public static void Build(ActionCallback copyCallback) + { + List downloads = new List(); + foreach (ModelEntry modelEntry in modelEntries) + { + if (!modelEntry.includeInBuild) continue; + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (File.Exists(target)) continue; + if (!downloadOnStart) copyCallback(modelEntry.path, target); + else downloads.Add(new StringPair { source = modelEntry.url, target = modelEntry.filename }); + } + + if (downloads.Count > 0) + { + string downloadJSON = JsonUtility.ToJson(new ListStringPair { pairs = downloads }, true); + using (FileStream fs = new FileStream(LLMUnitySetup.BuildFile, FileMode.Create, FileAccess.Write)) + { + using (BinaryWriter writer = new BinaryWriter(fs)) writer.Write(downloadJSON); + } + } + } + +#endif } } -#endif diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index e6e70354..1f68d942 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -60,6 +60,20 @@ public NotImplementedException() : base("The method needs to be implemented by s public delegate void Callback(T message); public delegate Task TaskCallback(T message); public delegate T2 ContentCallback(T message); + public delegate void ActionCallback(string source, string target); + + [Serializable] + public struct StringPair + { + public string source; + public string target; + } + + [Serializable] + public class ListStringPair + { + public List pairs; + } /// \endcond /// @ingroup utils @@ -82,7 +96,9 @@ public class LLMUnitySetup /// Model download path public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); /// Temporary dir for build - public static string buildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); + public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); + /// Temporary dir for build + public static string BuildFile = GetAssetPath("LLMUnityBuild.bin"); /// Default models for download [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] @@ -294,7 +310,7 @@ public static void MovePath(string source, string target) public static bool DeletePath(string path) { - if (!IsSubPath(path, GetAssetPath()) && !IsSubPath(path, buildTempDir)) + if (!IsSubPath(path, GetAssetPath()) && !IsSubPath(path, BuildTempDir)) { LogError($"Safeguard: {path} will not be deleted because it may not be safe"); return false; diff --git a/Runtime/ResumingWebClient.cs b/Runtime/ResumingWebClient.cs index b6e8d4d5..8a678d5c 100644 --- a/Runtime/ResumingWebClient.cs +++ b/Runtime/ResumingWebClient.cs @@ -19,7 +19,12 @@ public ResumingWebClient() _context = SynchronizationContext.Current ?? new SynchronizationContext(); } - private long GetRemoteFileSizeAsync(Uri address) + public long GetURLFileSize(string address) + { + return GetURLFileSize(new Uri(address)); + } + + public long GetURLFileSize(Uri address) { WebRequest request = GetWebRequest(address); request.Method = "HEAD"; @@ -45,7 +50,7 @@ public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resum WebRequest request = GetWebRequest(address); if (request is HttpWebRequest webRequest && bytesToSkip > 0) { - long remoteFileSize = GetRemoteFileSizeAsync(address); + long remoteFileSize = GetURLFileSize(address); if (bytesToSkip >= remoteFileSize) { LLMUnitySetup.Log($"File is already fully downloaded: {fileName}"); diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 681f6dfb..c697fb46 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -8,7 +8,6 @@ namespace LLMUnitySamples { public class AndroidDemo : MonoBehaviour { - public LLM llm; public LLMCharacter llmCharacter; public GameObject ChatPanel; @@ -32,14 +31,13 @@ async Task ShowDownloadScreen() { ChatPanel.SetActive(false); DownloadPanel.SetActive(true); - await llm.WaitUntilModelDownloaded(SetProgress); + await LLM.WaitUntilModelsDownloaded(SetProgress); DownloadPanel.SetActive(false); ChatPanel.SetActive(true); } async Task WarmUp() { - llm.SetTemplate("alpaca"); cores = LLMUnitySetup.AndroidGetNumBigCores(); AIText.text += $"Warming up the model...\nWill use {cores} cores"; await llmCharacter.Warmup(); From a2b97556ba2d699a49b646bb48133012877093ab Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:22:59 +0300 Subject: [PATCH 048/105] extract files on android --- Runtime/LLM.cs | 11 ++++++++++- Runtime/LLMCharacter.cs | 3 ++- Runtime/LLMManager.cs | 3 ++- Runtime/LLMUnitySetup.cs | 10 ++++++---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 4e111541..0d6f1d2b 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -113,7 +113,7 @@ public async void Awake() await LLMManager.DownloadModels(); #endif modelsDownloaded = true; - // if (Application.platform == RuntimePlatform.Android) await AndroidExtractModels(); + await AndroidSetup(); string arguments = GetLlamaccpArguments(); if (arguments == null) return; if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); @@ -122,6 +122,15 @@ public async void Awake() if (basePrompt != "") await SetBasePrompt(basePrompt); } + public async Task AndroidSetup() + { + if (Application.platform != RuntimePlatform.Android) return; + foreach (string path in new string[] {model, lora}) + { + if (path != "" && !File.Exists(LLMUnitySetup.GetAssetPath(path))) await LLMUnitySetup.AndroidExtractFile(path); + } + } + #if UNITY_EDITOR public LLMManager llmManager = new LLMManager(); diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index cd3a192c..4a80fd9c 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -319,11 +319,12 @@ public async Task LoadTemplate() /// Set the grammar file of the LLMCharacter /// /// path to the grammar file - public void SetGrammar(string path) + public async void SetGrammar(string path) { #if UNITY_EDITOR if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path); #endif + if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(path); grammar = path; InitGrammar(); } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index e644d72b..6d2fd0d2 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -44,6 +44,7 @@ public static void SetDownloadProgress(float progress) public static async Task DownloadModels() { + if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(LLMUnitySetup.BuildFilename); if (!File.Exists(LLMUnitySetup.BuildFile)) return; List downloads = new List(); @@ -80,7 +81,7 @@ public static async Task DownloadModels() { currFileSize = fileSizes[pair.source]; await LLMUnitySetup.DownloadFile(pair.source, pair.target, false, null, SetDownloadProgress); - totalSize += currFileSize; + completedSize += currFileSize; } completedSize = totalSize; diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 1f68d942..44d443b0 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -97,8 +97,10 @@ public class LLMUnitySetup public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); /// Temporary dir for build public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); - /// Temporary dir for build - public static string BuildFile = GetAssetPath("LLMUnityBuild.bin"); + /// Name of file with build information for runtime + public static string BuildFilename = "LLMUnityBuild.bin"; + /// Path of file with build information for runtime + public static string BuildFile = GetAssetPath(BuildFilename); /// Default models for download [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] @@ -237,13 +239,13 @@ public static async Task DownloadFile( callback?.Invoke(savePath); } - public static async Task AndroidExtractFile(string assetName, bool overwrite = false, int chunkSize = 1024*1024) + public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024) { string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName; string target = GetAssetPath(assetName); if (!overwrite && File.Exists(target)) { - Log($"File {target} already exists"); + if (log) Log($"File {target} already exists"); return; } From c70808514c8b29e8702a73b6d6ff99e4c582f7e5 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:29:21 +0300 Subject: [PATCH 049/105] move android libraries to Plugins --- Runtime/LLMUnitySetup.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 44d443b0..2272ae4d 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -334,6 +334,14 @@ private static async Task DownloadLibrary() await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); AssetDatabase.StartAssetEditing(); ZipFile.ExtractToDirectory(libZip, libraryPath); + foreach (string librarySubPath in Directory.GetDirectories(libraryPath)) + { + if (Path.GetFileName(librarySubPath).StartsWith("android")) + { + string pluginPath = Path.Combine(Application.dataPath, "Plugins", "Android", Path.GetFileName(librarySubPath)); + Directory.Move(librarySubPath, pluginPath); + } + } AssetDatabase.StopAssetEditing(); File.Delete(libZip); } From dfd565b0ee356b0c0dd06cc7ea3b472df8cef235 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:36:23 +0300 Subject: [PATCH 050/105] move android library directly --- Runtime/LLMUnitySetup.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 2272ae4d..15351a20 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -334,6 +334,12 @@ private static async Task DownloadLibrary() await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); AssetDatabase.StartAssetEditing(); ZipFile.ExtractToDirectory(libZip, libraryPath); + string androidDir = Path.Combine(libraryPath, "android"); + if (Directory.Exists(androidDir)) + { + string androidPluginDir = Path.Combine(Application.dataPath, "Plugins", "Android", Path.GetFileName(libraryPath)); + Directory.Move(androidDir, androidPluginDir); + } foreach (string librarySubPath in Directory.GetDirectories(libraryPath)) { if (Path.GetFileName(librarySubPath).StartsWith("android")) From ceb1b3dea1b414527088e63df32c269bf5a2c04a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:36:48 +0300 Subject: [PATCH 051/105] bump LlamaLib to v1.1.6 --- Runtime/LLMUnitySetup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 15351a20..4291d4b3 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -86,7 +86,7 @@ public class LLMUnitySetup /// LLM for Unity version public static string Version = "v2.1.0"; /// LlamaLib version - public static string LlamaLibVersion = "v1.1.5"; + public static string LlamaLibVersion = "v1.1.6"; /// LlamaLib url public static string LlamaLibURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}/undreamai-{LlamaLibVersion}-llamacpp.zip"; /// LlamaLib path From 72766f5acf51fade3b77b927d05faf1c938af21d Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:49:18 +0300 Subject: [PATCH 052/105] create plugin dir first --- Runtime/LLMUnitySetup.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 4291d4b3..29d82961 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -337,8 +337,9 @@ private static async Task DownloadLibrary() string androidDir = Path.Combine(libraryPath, "android"); if (Directory.Exists(androidDir)) { - string androidPluginDir = Path.Combine(Application.dataPath, "Plugins", "Android", Path.GetFileName(libraryPath)); - Directory.Move(androidDir, androidPluginDir); + string androidPluginDir = Path.Combine(Application.dataPath, "Plugins", "Android"); + Directory.CreateDirectory(androidPluginDir); + Directory.Move(androidDir, Path.Combine(androidPluginDir, Path.GetFileName(libraryPath))); } foreach (string librarySubPath in Directory.GetDirectories(libraryPath)) { From 59a391ac0c96d43b3cd7b4328c60db2d7ccd377c Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 18:49:33 +0300 Subject: [PATCH 053/105] remove null warning --- Runtime/ResumingWebClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/ResumingWebClient.cs b/Runtime/ResumingWebClient.cs index 8a678d5c..b282caa2 100644 --- a/Runtime/ResumingWebClient.cs +++ b/Runtime/ResumingWebClient.cs @@ -35,7 +35,7 @@ public long GetURLFileSize(Uri address) public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resume = false, Callback progressCallback = null) { var tcs = new TaskCompletionSource(address); - FileStream? fs = null; + FileStream fs = null; long bytesToSkip = 0; try From 12f0d3902bb570dad1ff2ba092d36c964c84b516 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 19:35:48 +0300 Subject: [PATCH 054/105] allow only one download access --- Runtime/LLMManager.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 6d2fd0d2..c01eec8d 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -32,6 +32,8 @@ public class LLMManager { public static float downloadProgress = 1; public static List> downloadProgressCallbacks = new List>(); + static Task downloadModelsTask; + static readonly object lockObject = new object(); static long totalSize; static long currFileSize; static long completedSize; @@ -42,7 +44,16 @@ public static void SetDownloadProgress(float progress) foreach (Callback downloadProgressCallback in downloadProgressCallbacks) downloadProgressCallback?.Invoke(downloadProgress); } - public static async Task DownloadModels() + public static Task DownloadModels() + { + lock (lockObject) + { + if (downloadModelsTask == null) downloadModelsTask = DownloadModelsOnce(); + } + return downloadModelsTask; + } + + public static async Task DownloadModelsOnce() { if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(LLMUnitySetup.BuildFilename); if (!File.Exists(LLMUnitySetup.BuildFile)) return; From 0ff779060121b2e302e77ac6f6e57c3f5d98622e Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 19:53:09 +0300 Subject: [PATCH 055/105] capture and expose download errors --- Runtime/LLM.cs | 14 +++++++++----- Runtime/LLMManager.cs | 15 ++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 0d6f1d2b..544d9727 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -79,8 +79,10 @@ public class LLM : MonoBehaviour public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; + /// Boolean set to true if the models were not downloaded successfully. + public static bool downloadFailed { get; protected set; } = false; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. - public static bool modelsDownloaded { get; protected set; } = false; + public static bool downloadComplete { get; protected set; } = false; /// the LLM model to use. /// Models with .gguf format are allowed. @@ -110,9 +112,10 @@ public async void Awake() { if (!enabled) return; #if !UNITY_EDITOR - await LLMManager.DownloadModels(); + downloadFailed = !await LLMManager.DownloadModels(); #endif - modelsDownloaded = true; + downloadComplete = true; + if (downloadFailed) return; await AndroidSetup(); string arguments = GetLlamaccpArguments(); if (arguments == null) return; @@ -147,10 +150,11 @@ public async Task WaitUntilReady() while (!started) await Task.Yield(); } - public static async Task WaitUntilModelsDownloaded(Callback downloadProgressCallback = null) + public static async Task WaitUntilModelsDownloaded(Callback downloadProgressCallback = null) { if (downloadProgressCallback != null) LLMManager.downloadProgressCallbacks.Add(downloadProgressCallback); - while (!modelsDownloaded) await Task.Yield(); + while (!downloadComplete) await Task.Yield(); + return !downloadFailed; } /// diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index c01eec8d..efbb33d7 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -32,7 +32,7 @@ public class LLMManager { public static float downloadProgress = 1; public static List> downloadProgressCallbacks = new List>(); - static Task downloadModelsTask; + static Task downloadModelsTask; static readonly object lockObject = new object(); static long totalSize; static long currFileSize; @@ -44,7 +44,7 @@ public static void SetDownloadProgress(float progress) foreach (Callback downloadProgressCallback in downloadProgressCallbacks) downloadProgressCallback?.Invoke(downloadProgress); } - public static Task DownloadModels() + public static Task DownloadModels() { lock (lockObject) { @@ -53,10 +53,10 @@ public static Task DownloadModels() return downloadModelsTask; } - public static async Task DownloadModelsOnce() + public static async Task DownloadModelsOnce() { if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(LLMUnitySetup.BuildFilename); - if (!File.Exists(LLMUnitySetup.BuildFile)) return; + if (!File.Exists(LLMUnitySetup.BuildFile)) return true; List downloads = new List(); using (FileStream fs = new FileStream(LLMUnitySetup.BuildFile, FileMode.Open, FileAccess.Read)) @@ -71,7 +71,7 @@ public static async Task DownloadModelsOnce() } } } - if (downloads.Count == 0) return; + if (downloads.Count == 0) return true; try { @@ -100,9 +100,10 @@ public static async Task DownloadModelsOnce() } catch (Exception ex) { - LLMUnitySetup.LogError($"Error downloading the models"); - throw ex; + LLMUnitySetup.LogError($"Error downloading the models: {ex.Message}"); + return false; } + return true; } #if UNITY_EDITOR From a8d2de05bd85dc54428deb062e25011439b46d4a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 19:53:36 +0300 Subject: [PATCH 056/105] fix android dll name --- Runtime/LLMLib.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMLib.cs b/Runtime/LLMLib.cs index 95cd5576..c8d49ccd 100644 --- a/Runtime/LLMLib.cs +++ b/Runtime/LLMLib.cs @@ -404,7 +404,7 @@ public static string GetArchitecturePath(string arch) } else if (Application.platform == RuntimePlatform.Android) { - return "libundreamai_android_plugin.so"; + return "libundreamai_android.so"; } else { From 64fffab9923188093d4d7c3f1c19e437d0b72753 Mon Sep 17 00:00:00 2001 From: amakropoulos Date: Fri, 26 Jul 2024 16:59:49 +0000 Subject: [PATCH 057/105] update changelogs --- CHANGELOG.md | 1 + CHANGELOG.release.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7895c1ce..6c2185b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### 🚀 Features - Android deployment (PR: #194) +- LLM model selector with download store (PR: #196) ## v2.0.3 diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index 650d0934..62326099 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -1,4 +1,5 @@ ### 🚀 Features - Android deployment (PR: #194) +- LLM model selector with download store (PR: #196) From 818d3ea15d552c4b35ef07830938aea684301d59 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 20:04:07 +0300 Subject: [PATCH 058/105] show network erro if it happens --- Samples~/AndroidDemo/AndroidDemo.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index c697fb46..3ad2d3fb 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -1,7 +1,6 @@ using UnityEngine; using LLMUnity; using UnityEngine.UI; -using System.Collections.Generic; using System.Threading.Tasks; namespace LLMUnitySamples @@ -13,6 +12,7 @@ public class AndroidDemo : MonoBehaviour public GameObject ChatPanel; public InputField playerText; public Text AIText; + public GameObject ErrorText; public GameObject DownloadPanel; public Scrollbar progressBar; @@ -23,17 +23,24 @@ async void Start() { playerText.onSubmit.AddListener(onInputFieldSubmit); playerText.interactable = false; - await ShowDownloadScreen(); - await WarmUp(); + await DownloadThenWarmup(); } - async Task ShowDownloadScreen() + async Task DownloadThenWarmup() { ChatPanel.SetActive(false); DownloadPanel.SetActive(true); - await LLM.WaitUntilModelsDownloaded(SetProgress); - DownloadPanel.SetActive(false); - ChatPanel.SetActive(true); + bool downloadOK = await LLM.WaitUntilModelsDownloaded(SetProgress); + if (!downloadOK) + { + ErrorText.SetActive(true); + } + else + { + DownloadPanel.SetActive(false); + ChatPanel.SetActive(true); + await WarmUp(); + } } async Task WarmUp() From cc45bce985eb4f7c06074a7a538fb91ac953f2ee Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 20:12:45 +0300 Subject: [PATCH 059/105] show info about download on start --- Samples~/AndroidDemo/AndroidDemo.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 3ad2d3fb..1eb81332 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -90,6 +90,7 @@ public void ExitGame() } bool onValidateWarning = true; + bool onValidateInfo = true; void OnValidate() { if (onValidateWarning && !llmCharacter.remote && llmCharacter.llm != null && llmCharacter.llm.model == "") @@ -97,6 +98,11 @@ void OnValidate() Debug.LogWarning($"Please select a model in the {llmCharacter.llm.gameObject.name} GameObject!"); onValidateWarning = false; } + if (onValidateInfo) + { + Debug.Log($"Select 'Download On Start' in the {llmCharacter.llm.gameObject.name} GameObject to download the models when the app starts."); + onValidateInfo = false; + } } } } From 517ea5791faca90eed5f541bd492bb721a78e88f Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 20:20:26 +0300 Subject: [PATCH 060/105] set model from load if none --- Editor/LLMEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index b9354cfa..8b72373e 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -218,7 +218,8 @@ async Task createButtons() string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); if (!string.IsNullOrEmpty(path)) { - LLMManager.LoadModel(path); + string filename = LLMManager.LoadModel(path); + SetModelIfNone(filename, false); UpdateModels(); } }; From ed4d026b4c03c7840695e0e2714b236c07200a90 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 20:57:09 +0300 Subject: [PATCH 061/105] add llama3 7B and Qwen2 0.5B models --- Runtime/LLMUnitySetup.cs | 9 +++++---- Third Party Notices.md | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 29d82961..6581f629 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -105,10 +105,11 @@ public class LLMUnitySetup /// Default models for download [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] { - ("Mistral 7B Instruct v0.2 (medium, best overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), - ("OpenHermes 2.5 7B (medium, best for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), - ("Phi 3 (small, great)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), - ("Test", "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"), + ("Llama 3 7B (medium, best overall)", "https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf?download=true"), + ("Mistral 7B Instruct v0.2 (medium, great overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), + ("OpenHermes 2.5 7B (medium, good for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), + ("Phi 3 (small, great small model)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), + ("Qwen 2 0.5B (tiny, useful for mobile)", "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf?download=true"), }; /// Add callback function to call for error logs diff --git a/Third Party Notices.md b/Third Party Notices.md index 0c482440..ca86d120 100644 --- a/Third Party Notices.md +++ b/Third Party Notices.md @@ -26,6 +26,22 @@ License: [link](https://github.com/Mozilla-Ocho/llamafile/blob/main/LICENSE) The following models can be downloaded with LLMUnity: +### meta-llama/Meta-Llama-3-8B-Instruct + +Developer: Meta
+Origin: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct)
+License Type: "llama3"
+License: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE) + +##### modified by: lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF + +Developer: LM Studio
+Origin: [link](https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF)
+License Type: "llama3"
+License: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE) + +
+ ### mistralai/Mistral-7B-Instruct-v0.2 Developer: Mistral AI
@@ -65,6 +81,15 @@ Origin: [link](https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF) License: [link](https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF) +
+ +### Qwen/Qwen2-0.5B-Instruct-GGUF + +Developer: Qwen
+Origin: [link](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF)
+License Type: "Apache 2.0"
+License: [link](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/blob/main/LICENSE) + --- ## Testing From 1734703af492bf3167171ed1959b4d69108cd637 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 21:04:16 +0300 Subject: [PATCH 062/105] show license if not MIT/Apache 2 --- Editor/LLMEditor.cs | 12 ++++++------ Runtime/LLMUnitySetup.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 8b72373e..72920d0e 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -20,6 +20,7 @@ public class LLMEditor : PropertyEditor static int elementPadding = 10; static GUIContent trashIcon; static List modelOptions; + static List modelLicenses; static List modelURLs; string elementFocus = ""; bool showCustomURL = false; @@ -64,11 +65,7 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) } _ = AddLoadButtons(); bool downloadOnStart = EditorGUILayout.Toggle("Download on Start", LLMManager.downloadOnStart); - if (downloadOnStart != LLMManager.downloadOnStart) - { - LLMManager.downloadOnStart = downloadOnStart; - LLMManager.Save(); - } + if (downloadOnStart != LLMManager.downloadOnStart) LLMManager.SetDownloadOnStart(downloadOnStart); } public void AddModelSettings(SerializedObject llmScriptSO) @@ -93,11 +90,13 @@ static void ResetModelOptions() foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); modelOptions = new List(){"Download model", "Custom URL"}; modelURLs = new List(){null, null}; - foreach ((string name, string url) in LLMUnitySetup.modelOptions) + modelLicenses = new List(){null, null}; + foreach ((string name, string url, string license) in LLMUnitySetup.modelOptions) { if (url != null && existingOptions.Contains(url)) continue; modelOptions.Add(name); modelURLs.Add(url); + modelLicenses.Add(license); } } @@ -206,6 +205,7 @@ async Task createButtons() } else if (modelIndex > 1) { + if (modelLicenses[modelIndex] != null) Debug.LogWarning($"The {modelOptions[modelIndex]} model is released under the following license: {modelLicenses[modelIndex]}. By using this model, you agree to the terms of the license."); string filename = await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); SetModelIfNone(filename, false); UpdateModels(true); diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 6581f629..62d087ee 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -103,13 +103,13 @@ public class LLMUnitySetup public static string BuildFile = GetAssetPath(BuildFilename); /// Default models for download - [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] + [HideInInspector] public static readonly (string, string, string)[] modelOptions = new(string, string, string)[] { - ("Llama 3 7B (medium, best overall)", "https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf?download=true"), - ("Mistral 7B Instruct v0.2 (medium, great overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), - ("OpenHermes 2.5 7B (medium, good for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), - ("Phi 3 (small, great small model)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), - ("Qwen 2 0.5B (tiny, useful for mobile)", "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf?download=true"), + ("Llama 3 7B (medium, best overall)", "https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf?download=true", "https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE"), + ("Mistral 7B Instruct v0.2 (medium, great overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true", null), + ("OpenHermes 2.5 7B (medium, good for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true", null), + ("Phi 3 (small, great small model)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true", null), + ("Qwen 2 0.5B (tiny, useful for mobile)", "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf?download=true", null), }; /// Add callback function to call for error logs From b8f38d26ddc01279f91c1e43d18814ef11dad665 Mon Sep 17 00:00:00 2001 From: amakropoulos Date: Fri, 26 Jul 2024 18:06:35 +0000 Subject: [PATCH 063/105] update changelogs --- CHANGELOG.md | 1 + CHANGELOG.release.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2185b9..64b92ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Android deployment (PR: #194) - LLM model selector with download store (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) ## v2.0.3 diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index 62326099..e2d2d96f 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -2,4 +2,5 @@ - Android deployment (PR: #194) - LLM model selector with download store (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) From 86b093696ce11c248f07f2dcde59b31457dfa99f Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 21:07:16 +0300 Subject: [PATCH 064/105] add set download on start function --- Runtime/LLMManager.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index efbb33d7..5b8355b6 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -251,6 +251,21 @@ public static void SetIncludeInBuild(ModelEntry entry, bool includeInBuild) Save(); } + public static void SetDownloadOnStart(bool value) + { + downloadOnStart = value; + if (downloadOnStart) + { + bool warn = false; + foreach (ModelEntry entry in modelEntries) + { + if (entry.url == null || entry.url == "") warn = true; + } + if (warn) LLMUnitySetup.LogWarning("Some models do not have a URL and will be copied in the build. To resolve this fill in the URL field in the expanded view of the LLM Model list."); + } + Save(); + } + public static ModelEntry Get(string filename) { foreach (ModelEntry entry in modelEntries) @@ -341,7 +356,7 @@ public static void Build(ActionCallback copyCallback) if (!modelEntry.includeInBuild) continue; string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); if (File.Exists(target)) continue; - if (!downloadOnStart) copyCallback(modelEntry.path, target); + if (!downloadOnStart || modelEntry.url == null || modelEntry.url == "") copyCallback(modelEntry.path, target); else downloads.Add(new StringPair { source = modelEntry.url, target = modelEntry.filename }); } From 7a4a3e6d4fd4b76b8d83648acfc33d12ac978c18 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 21:09:37 +0300 Subject: [PATCH 065/105] start LLM always asynchronously --- Runtime/LLM.cs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 544d9727..82d9c905 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -45,27 +45,6 @@ public class LLM : MonoBehaviour [LLM] public bool debug = false; /// number of prompts that can happen in parallel (-1 = number of LLMCharacter objects) [LLMAdvanced] public int parallelPrompts = -1; - /// allows to start the server asynchronously. - /// This is useful to not block Unity while the server is initialised. - /// For example it can be used as follows: - /// \code - /// void Start(){ - /// StartCoroutine(Loading()); - /// ... - /// } - /// - /// IEnumerator Loading() - /// { - /// // show loading screen - /// while (!llm.started) - /// { - /// yield return null; - /// } - /// Debug.Log("Server is ready"); - /// } - /// \endcode - /// - [LLMAdvanced] public bool asynchronousStartup = true; /// select to not destroy the LLM GameObject when loading a new Scene. [LLMAdvanced] public bool dontDestroyOnLoad = true; /// Size of the prompt context (0 = context size of the model). @@ -119,8 +98,7 @@ public async void Awake() await AndroidSetup(); string arguments = GetLlamaccpArguments(); if (arguments == null) return; - if (asynchronousStartup) await Task.Run(() => StartLLMServer(arguments)); - else StartLLMServer(arguments); + await Task.Run(() => StartLLMServer(arguments)); if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); if (basePrompt != "") await SetBasePrompt(basePrompt); } From faed7159a8921f5c156b969fa8c4b9fd53bd0d24 Mon Sep 17 00:00:00 2001 From: amakropoulos Date: Fri, 26 Jul 2024 18:10:23 +0000 Subject: [PATCH 066/105] update changelogs --- CHANGELOG.md | 1 + CHANGELOG.release.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b92ff3..3faf8051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Android deployment (PR: #194) - LLM model selector with download store (PR: #196) - Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Start LLM always asynchronously (PR: #199) ## v2.0.3 diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index e2d2d96f..d658bcea 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -3,4 +3,5 @@ - Android deployment (PR: #194) - LLM model selector with download store (PR: #196) - Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Start LLM always asynchronously (PR: #199) From b69bc72596446c3e450b3da25e6bb8a3c095155a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 26 Jul 2024 21:12:21 +0300 Subject: [PATCH 067/105] remove async from test --- Tests/Runtime/TestLLM.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index e997a327..0647f917 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -36,7 +36,6 @@ public async Task Init() llm.SetModel(fullModelPath); llm.parallelPrompts = 1; llm.SetTemplate("alpaca"); - llm.asynchronousStartup = false; llmCharacter = gameObject.AddComponent(); llmCharacter.llm = llm; From 8c2473d97ae825cca4d217c921cab5cbdb2ff2b6 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 27 Jul 2024 00:45:27 +0300 Subject: [PATCH 068/105] set chatml as default for qwen --- Runtime/LLMChatTemplates.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMChatTemplates.cs b/Runtime/LLMChatTemplates.cs index 39f7fa9e..f6aa22cc 100644 --- a/Runtime/LLMChatTemplates.cs +++ b/Runtime/LLMChatTemplates.cs @@ -207,7 +207,7 @@ public class ChatMLTemplate : ChatTemplate { public override string GetName() { return "chatml"; } public override string GetDescription() { return "chatml (most widely used)"; } - public override string[] GetNameMatches() { return new string[] {"chatml", "hermes"}; } + public override string[] GetNameMatches() { return new string[] {"chatml", "hermes", "qwen"}; } public override string[] GetChatTemplateMatches() { return new string[] {"{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"}; } protected override string SystemPrefix() { return "<|im_start|>system\n"; } From 8c894e8f34d0da60157b9ccbba240a5272250868 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 27 Jul 2024 00:54:49 +0300 Subject: [PATCH 069/105] allow to load a new model with SetModel --- Runtime/LLM.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 82d9c905..daea3f63 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -146,7 +146,8 @@ public void SetModel(string path) // set the model and enable the model editor properties model = path; #if UNITY_EDITOR - SetTemplate(LLMManager.Get(path).chatTemplate); + model = LLMManager.LoadModel(path); + SetTemplate(LLMManager.Get(model).chatTemplate); if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif } From 6bc4e084061b2129aa46116448c8c85750af237e Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 27 Jul 2024 01:01:56 +0300 Subject: [PATCH 070/105] add AI Emotional Girlfriend game --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 16bf1921..4221ba15 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger - [Nameless Souls of the Void](https://unicorninteractive.itch.io/nameless-souls-of-the-void) - [Murder in Aisle 4](https://roadedlich.itch.io/murder-in-aisle-4) - [Finicky Food Delivery AI](https://helixngc7293.itch.io/finicky-food-delivery-ai) +- [AI Emotional Girlfriend](https://whynames.itch.io/aiemotionalgirlfriend) ## Setup _Method 1: Install using the asset store_ From 4f4dc5e464ec0013b1da9e4e4f1e3cca1f96143a Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 27 Jul 2024 01:05:31 +0300 Subject: [PATCH 071/105] remove the CPU core display --- Samples~/AndroidDemo/AndroidDemo.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 1eb81332..58b166cd 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -17,7 +17,6 @@ public class AndroidDemo : MonoBehaviour public GameObject DownloadPanel; public Scrollbar progressBar; public Text progressText; - int cores; async void Start() { @@ -45,10 +44,9 @@ async Task DownloadThenWarmup() async Task WarmUp() { - cores = LLMUnitySetup.AndroidGetNumBigCores(); - AIText.text += $"Warming up the model...\nWill use {cores} cores"; + AIText.text += $"Warming up the model..."; await llmCharacter.Warmup(); - AIText.text = $"Ready when you are ({cores} cores)!"; + AIText.text = ""; AIReplyComplete(); } From 26e4953a234a779b5c12b861358c5fde014f432f Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sat, 27 Jul 2024 01:09:02 +0300 Subject: [PATCH 072/105] add AndroidDemo sample to Readmes --- README.md | 1 + package.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 4221ba15..d9c8dfb6 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ The [Samples~](Samples~) folder contains several examples of interaction 🤖: - [MultipleCharacters](Samples~/MultipleCharacters): Demonstrates a simple interaction using multiple AI characters - [KnowledgeBaseGame](Samples~/KnowledgeBaseGame): Simple detective game using a knowledge base to provide information to the LLM based on [google/mysteryofthreebots](https://github.com/google/mysteryofthreebots) - [ChatBot](Samples~/ChatBot): Demonstrates interaction between a player and a AI with a UI similar to a messaging app (see image below) +- [AndroidDemo](Samples~/AndroidDemo): Example Android app with an initial screen with model download progress diff --git a/package.json b/package.json index 61a9f4ae..4f00e89d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,11 @@ "displayName": "ChatBot", "description": "Interaction between a player and a AI with a UI similar to a messaging app", "path": "Samples~/ChatBot" + }, + { + "displayName": "AndroidDemo", + "description": "Example Android app with an initial screen with model download progress", + "path": "Samples~/AndroidDemo" } ], "author": { From 16a2c3038050d6b71041efdcd1ee2b21ce971438 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 30 Jul 2024 08:16:46 +0300 Subject: [PATCH 073/105] add note on RAGSearchUnity setup --- Samples~/KnowledgeBaseGame/Scene.unity | 84 +++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/Samples~/KnowledgeBaseGame/Scene.unity b/Samples~/KnowledgeBaseGame/Scene.unity index 5f01ed7a..3be84471 100644 --- a/Samples~/KnowledgeBaseGame/Scene.unity +++ b/Samples~/KnowledgeBaseGame/Scene.unity @@ -1025,6 +1025,7 @@ MonoBehaviour: m_EditorClassIdentifier: CharacterSelect: {fileID: 1821864875} PlayerText: {fileID: 283994679} + SetupText: {fileID: 2024418435} AIText: {fileID: 681685488} ButlerText: {fileID: 4900000, guid: c2c64001c142dfdbab3c68bbf78335ed, type: 3} MaidText: {fileID: 4900000, guid: 70839d4b753f9ff66a48a0d931db2629, type: 3} @@ -2856,6 +2857,7 @@ RectTransform: - {fileID: 964460962} - {fileID: 1723006217} - {fileID: 1468962045} + - {fileID: 2024418434} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -7236,6 +7238,87 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2019606177} m_CullTransparentMesh: 1 +--- !u!1 &2024418433 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2024418434} + - component: {fileID: 2024418436} + - component: {fileID: 2024418435} + m_Layer: 5 + m_Name: SetupText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2024418434 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 898411689} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -65.892715, y: -447.78424} + m_SizeDelta: {x: 1154.2146, y: 70} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2024418435 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'The sample requires the RAGSearchUnity asset from: + + https://github.com/undreamai/RAGSearchUnity' +--- !u!222 &2024418436 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_CullTransparentMesh: 1 --- !u!1 &2038337339 GameObject: m_ObjectHideFlags: 0 @@ -7458,7 +7541,6 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 model: lora: From 0336d0214e3226cc82d490ae5a3d9700144bdf21 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 30 Jul 2024 08:17:21 +0300 Subject: [PATCH 074/105] hide note on RAGSearchUnity setup on awake --- Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs b/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs index eda4ea21..e6f3bc29 100644 --- a/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs +++ b/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs @@ -188,6 +188,7 @@ public class KnowledgeBaseGameUI : MonoBehaviour { public Dropdown CharacterSelect; public InputField PlayerText; + public Text SetupText; public Text AIText; public TextAsset ButlerText; @@ -215,6 +216,11 @@ public class KnowledgeBaseGameUI : MonoBehaviour public Dropdown Answer2; public Dropdown Answer3; + void Awake() + { + if (SetupText != null) SetupText.gameObject.SetActive(false); + } + protected void Start() { AddListeners(); From 6f37a8e6e4cca2cef59ea8a0a27834c3e04fa0a7 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Tue, 30 Jul 2024 08:17:53 +0300 Subject: [PATCH 075/105] assembly definition to avoid errors with RAGSearchUnity missing --- .../KnowledgeBaseGame/KnowledgeBase.asmdef | 28 +++++++++++++++++++ .../KnowledgeBase.asmdef.meta | 7 +++++ 2 files changed, 35 insertions(+) create mode 100644 Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef create mode 100644 Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef new file mode 100644 index 00000000..e8831c36 --- /dev/null +++ b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef @@ -0,0 +1,28 @@ +{ + "name": "KnowledgeBase", + "rootNamespace": "", + "references": [ + "undream.llmunity.Runtime", + "undream.RAGSearchUnity.Runtime", + "Unity.Sentis", + "HuggingFace.SharpTransformers", + "Cloud.Unum.USearch" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [ + "RAGSEARCHUNITY" + ], + "versionDefines": [ + { + "name": "ai.undream.ragsearchunity", + "expression": "1.0.0", + "define": "RAGSEARCHUNITY" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta new file mode 100644 index 00000000..35a5d9b7 --- /dev/null +++ b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 42f2ee663135398278c988534b3ae0b3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 0c95c3fefb8efd758ee8745aa1c4a889e0321d31 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:02:30 +0300 Subject: [PATCH 076/105] include LLMManager functions on build and simplify build, allow SetModel for LLMManager or StreamingAssets files --- Runtime/LLM.cs | 97 ++++++++++------ Runtime/LLMBuilder.cs | 3 +- Runtime/LLMCharacter.cs | 2 +- Runtime/LLMManager.cs | 232 +++++++++++++++++++++------------------ Runtime/LLMUnitySetup.cs | 10 +- 5 files changed, 199 insertions(+), 145 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index daea3f63..505bf24e 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -59,9 +59,9 @@ public class LLM : MonoBehaviour /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; /// Boolean set to true if the models were not downloaded successfully. - public static bool downloadFailed { get; protected set; } = false; + public static bool modelSetupFailed { get; protected set; } = false; /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. - public static bool downloadComplete { get; protected set; } = false; + public static bool modelSetupComplete { get; protected set; } = false; /// the LLM model to use. /// Models with .gguf format are allowed. @@ -80,9 +80,15 @@ public class LLM : MonoBehaviour StreamWrapper logStreamWrapper = null; Thread llmThread = null; List streamWrappers = new List(); + public LLMManager llmManager = new LLMManager(); /// \endcond + public LLM() + { + LLMManager.Register(this); + } + /// /// The Unity Awake function that starts the LLM server. /// The server can be started asynchronously if the asynchronousStartup option is set. @@ -91,13 +97,21 @@ public async void Awake() { if (!enabled) return; #if !UNITY_EDITOR - downloadFailed = !await LLMManager.DownloadModels(); + modelSetupFailed = !await LLMManager.Setup(); #endif - downloadComplete = true; - if (downloadFailed) return; + modelSetupComplete = true; + if (modelSetupFailed) + { + failed = true; + return; + } await AndroidSetup(); string arguments = GetLlamaccpArguments(); - if (arguments == null) return; + if (arguments == null) + { + failed = true; + return; + } await Task.Run(() => StartLLMServer(arguments)); if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); if (basePrompt != "") await SetBasePrompt(basePrompt); @@ -112,27 +126,47 @@ public async Task AndroidSetup() } } -#if UNITY_EDITOR - - public LLMManager llmManager = new LLMManager(); - - public LLM() + public async Task WaitUntilReady() { - LLMManager.Register(this); + while (!started) await Task.Yield(); } -#endif + public static async Task WaitUntilModelSetup(Callback downloadProgressCallback = null) + { + if (downloadProgressCallback != null) LLMManager.downloadProgressCallbacks.Add(downloadProgressCallback); + while (!modelSetupComplete) await Task.Yield(); + return !modelSetupFailed; + } - public async Task WaitUntilReady() + public string GetModelLoraPath(string path) { - while (!started) await Task.Yield(); + string modelPath = LLMUnitySetup.GetAssetPath(path); +#if UNITY_EDITOR + if (!File.Exists(modelPath)) + { + ModelEntry modelEntry = LLMManager.Get(path); + if (modelEntry != null) modelPath = modelEntry.path; + } +#endif + return modelPath; } - public static async Task WaitUntilModelsDownloaded(Callback downloadProgressCallback = null) + public string SetModelLoraPath(string path, bool lora) { - if (downloadProgressCallback != null) LLMManager.downloadProgressCallbacks.Add(downloadProgressCallback); - while (!downloadComplete) await Task.Yield(); - return !downloadFailed; + ModelEntry modelEntry = LLMManager.Get(path); + if (modelEntry != null) return modelEntry.filename; + string assetPath = LLMUnitySetup.GetAssetPath(path); + if (LLMUnitySetup.IsSubPath(assetPath, LLMUnitySetup.GetAssetPath()) && File.Exists(assetPath)) return path; + + string errorMessage; + string modelType = lora ? "Lora" : "Model"; + if (File.Exists(path)) errorMessage = $"The {modelType} path needs to be relative to the StreamingAssets folder."; + else errorMessage = $"The {modelType} path was not found."; + errorMessage += " Use one of the following methods:"; + errorMessage += $"\n-Copy the {modelType} inside the StreamingAssets folder and use its relative path or"; + errorMessage += $"\n-Load the {modelType} with the LLMManager: `string filename=LLMManager.Load{modelType}(path); llm.Set{modelType}(filename)`"; + LLMUnitySetup.LogError(errorMessage); + return ""; } /// @@ -143,11 +177,14 @@ public static async Task WaitUntilModelsDownloaded(Callback downloa /// path to model to use (.gguf format) public void SetModel(string path) { - // set the model and enable the model editor properties - model = path; + model = SetModelLoraPath(path, false); + if (!string.IsNullOrEmpty(model)) + { + ModelEntry modelEntry = LLMManager.Get(model); + string template = modelEntry != null ? modelEntry.chatTemplate : ChatTemplate.FromGGUF(GetModelLoraPath(model)); + SetTemplate(template); + } #if UNITY_EDITOR - model = LLMManager.LoadModel(path); - SetTemplate(LLMManager.Get(model).chatTemplate); if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif } @@ -160,7 +197,7 @@ public void SetModel(string path) /// path to LORA model to use (.bin format) public void SetLora(string path) { - lora = path; + lora = SetModelLoraPath(path, true); #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif @@ -193,11 +230,7 @@ protected virtual string GetLlamaccpArguments() LLMUnitySetup.LogError("No model file provided!"); return null; } -#if UNITY_EDITOR - string modelPath = LLMManager.Get(model).path; -#else - string modelPath = LLMUnitySetup.GetAssetPath(model); -#endif + string modelPath = GetModelLoraPath(model); if (!File.Exists(modelPath)) { LLMUnitySetup.LogError($"File {modelPath} not found!"); @@ -206,11 +239,7 @@ protected virtual string GetLlamaccpArguments() string loraPath = ""; if (lora != "") { -#if UNITY_EDITOR - loraPath = LLMManager.Get(lora).path; -#else - loraPath = LLMUnitySetup.GetAssetPath(lora); -#endif + loraPath = GetModelLoraPath(lora); if (!File.Exists(loraPath)) { LLMUnitySetup.LogError($"File {loraPath} not found!"); diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs index db7e80c3..560d8901 100644 --- a/Runtime/LLMBuilder.cs +++ b/Runtime/LLMBuilder.cs @@ -84,9 +84,8 @@ public static void HideLibraryPlatforms(string platform) public static void BuildModels() { - LLMUnitySetup.DeletePath(LLMUnitySetup.BuildFile); LLMManager.Build(CopyActionAddMeta); - if (File.Exists(LLMUnitySetup.BuildFile)) AddTargetPair(LLMUnitySetup.BuildFile); + if (File.Exists(LLMUnitySetup.LLMManagerPath)) AddMovedPair("", LLMUnitySetup.LLMManagerPath); } public static void Reset() diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 4a80fd9c..8d35559b 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -324,7 +324,7 @@ public async void SetGrammar(string path) #if UNITY_EDITOR if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path); #endif - if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(path); + await LLMUnitySetup.AndroidExtractAsset(path); grammar = path; InitGrammar(); } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 5b8355b6..599f0e46 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -7,7 +7,6 @@ namespace LLMUnity { -#if UNITY_EDITOR [Serializable] public class ModelEntry { @@ -18,6 +17,26 @@ public class ModelEntry public string chatTemplate; public string url; public bool includeInBuild; + + + public ModelEntry(string path, bool lora = false, string label = null, string url = null) + { + filename = Path.GetFileName(path); + this.label = label == null ? filename : label; + this.lora = lora; + this.path = Path.GetFullPath(path).Replace('\\', '/'); + chatTemplate = lora ? null : ChatTemplate.FromGGUF(this.path); + this.url = url; + includeInBuild = true; + } + + public ModelEntry OnlyRequiredFields() + { + ModelEntry entry = (ModelEntry)MemberwiseClone(); + entry.label = null; + entry.path = entry.filename; + return entry; + } } [Serializable] @@ -26,13 +45,16 @@ public class LLMManagerStore public bool downloadOnStart; public List modelEntries; } -#endif public class LLMManager { + public static bool downloadOnStart = false; + public static List modelEntries = new List(); + static List llms = new List(); + public static float downloadProgress = 1; public static List> downloadProgressCallbacks = new List>(); - static Task downloadModelsTask; + static Task SetupTask; static readonly object lockObject = new object(); static long totalSize; static long currFileSize; @@ -44,32 +66,26 @@ public static void SetDownloadProgress(float progress) foreach (Callback downloadProgressCallback in downloadProgressCallbacks) downloadProgressCallback?.Invoke(downloadProgress); } - public static Task DownloadModels() + public static Task Setup() { lock (lockObject) { - if (downloadModelsTask == null) downloadModelsTask = DownloadModelsOnce(); + if (SetupTask == null) SetupTask = SetupOnce(); } - return downloadModelsTask; + return SetupTask; } - public static async Task DownloadModelsOnce() + public static async Task SetupOnce() { - if (Application.platform == RuntimePlatform.Android) await LLMUnitySetup.AndroidExtractFile(LLMUnitySetup.BuildFilename); - if (!File.Exists(LLMUnitySetup.BuildFile)) return true; + await LLMUnitySetup.AndroidExtractAsset(LLMUnitySetup.LLMManagerPath); + LoadFromDisk(); + if (!downloadOnStart) return true; List downloads = new List(); - using (FileStream fs = new FileStream(LLMUnitySetup.BuildFile, FileMode.Open, FileAccess.Read)) + foreach (ModelEntry modelEntry in modelEntries) { - using (BinaryReader reader = new BinaryReader(fs)) - { - List downloadsToDo = JsonUtility.FromJson(reader.ReadString()).pairs; - foreach (StringPair pair in downloadsToDo) - { - string target = LLMUnitySetup.GetAssetPath(pair.target); - if (!File.Exists(target)) downloads.Add(new StringPair {source = pair.source, target = target}); - } - } + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (!File.Exists(target)) downloads.Add(new StringPair {source = modelEntry.url, target = target}); } if (downloads.Count == 0) return true; @@ -106,14 +122,76 @@ public static async Task DownloadModelsOnce() return true; } + public static void SetTemplate(string filename, string chatTemplate) + { + SetTemplate(Get(filename), chatTemplate); + } + + public static void SetTemplate(ModelEntry entry, string chatTemplate) + { + if (entry == null) return; + entry.chatTemplate = chatTemplate; + foreach (LLM llm in llms) + { + if (llm.model == entry.filename) llm.SetTemplate(chatTemplate); + } +#if UNITY_EDITOR + Save(); +#endif + } + + public static ModelEntry Get(string filename) + { + foreach (ModelEntry entry in modelEntries) + { + if (entry.filename == filename) return entry; + } + return null; + } + + public static int Num(bool lora) + { + int num = 0; + foreach (ModelEntry entry in modelEntries) + { + if (entry.lora == lora) num++; + } + return num; + } + + public static int NumModels() + { + return Num(false); + } + + public static int NumLoras() + { + return Num(true); + } + + public static void Register(LLM llm) + { + llms.Add(llm); + } + + public static void Unregister(LLM llm) + { + llms.Remove(llm); + } + + public static void LoadFromDisk() + { + if (!File.Exists(LLMUnitySetup.LLMManagerPath)) return; + LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.LLMManagerPath)); + downloadOnStart = store.downloadOnStart; + modelEntries = store.modelEntries; + } + #if UNITY_EDITOR static string LLMManagerPref = "LLMManager"; - public static bool downloadOnStart = false; - public static List modelEntries = new List(); [HideInInspector] public static float modelProgress = 1; [HideInInspector] public static float loraProgress = 1; - static List llms = new List(); [InitializeOnLoadMethod] static void InitializeOnLoad() @@ -121,18 +199,10 @@ static void InitializeOnLoad() Load(); } - public static string AddEntry(string path, bool lora = false, string label = null, string url = null) + public static string AddEntry(ModelEntry entry) { - ModelEntry entry = new ModelEntry(); - entry.filename = Path.GetFileName(path.Split("?")[0]); - entry.label = label == null ? entry.filename : label; - entry.lora = lora; - entry.chatTemplate = lora ? null : ChatTemplate.FromGGUF(path); - entry.url = url; - entry.path = Path.GetFullPath(path).Replace('\\', '/'); - entry.includeInBuild = true; int indexToInsert = modelEntries.Count; - if (!lora) + if (!entry.lora) { for (int i = modelEntries.Count - 1; i >= 0; i--) { @@ -148,6 +218,11 @@ public static string AddEntry(string path, bool lora = false, string label = nul return entry.filename; } + public static string AddEntry(string path, bool lora = false, string label = null, string url = null) + { + return AddEntry(new ModelEntry(path, lora, label, url)); + } + public static async Task Download(string url, bool lora = false, string label = null) { foreach (ModelEntry entry in modelEntries) @@ -201,30 +276,14 @@ public static async Task DownloadLora(string url, string label = null) return await Download(url, true, label); } - public static string LoadModel(string url, string label = null) - { - return Load(url, false, label); - } - - public static string LoadLora(string url, string label = null) + public static string LoadModel(string path, string label = null) { - return Load(url, true, label); + return Load(path, false, label); } - public static void SetTemplate(string filename, string chatTemplate) + public static string LoadLora(string path, string label = null) { - SetTemplate(Get(filename), chatTemplate); - } - - public static void SetTemplate(ModelEntry entry, string chatTemplate) - { - if (entry == null) return; - entry.chatTemplate = chatTemplate; - foreach (LLM llm in llms) - { - if (llm.model == entry.filename) llm.SetTemplate(chatTemplate); - } - Save(); + return Load(path, true, label); } public static void SetURL(string filename, string url) @@ -266,15 +325,6 @@ public static void SetDownloadOnStart(bool value) Save(); } - public static ModelEntry Get(string filename) - { - foreach (ModelEntry entry in modelEntries) - { - if (entry.filename == filename) return entry; - } - return null; - } - public static void Remove(string filename) { Remove(Get(filename)); @@ -292,36 +342,6 @@ public static void Remove(ModelEntry entry) } } - public static int Num(bool lora) - { - int num = 0; - foreach (ModelEntry entry in modelEntries) - { - if (entry.lora == lora) num++; - } - return num; - } - - public static int NumModels() - { - return Num(false); - } - - public static int NumLoras() - { - return Num(true); - } - - public static void Register(LLM llm) - { - llms.Add(llm); - } - - public static void Unregister(LLM llm) - { - llms.Remove(llm); - } - public static void SetModelProgress(float progress) { modelProgress = progress; @@ -334,8 +354,8 @@ public static void SetLoraProgress(float progress) public static void Save() { - string pref = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true); - PlayerPrefs.SetString(LLMManagerPref, pref); + string json = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true); + PlayerPrefs.SetString(LLMManagerPref, json); PlayerPrefs.Save(); } @@ -348,25 +368,27 @@ public static void Load() modelEntries = store.modelEntries; } - public static void Build(ActionCallback copyCallback) + public static void SaveToDisk() { - List downloads = new List(); + List modelEntriesBuild = new List(); foreach (ModelEntry modelEntry in modelEntries) { if (!modelEntry.includeInBuild) continue; - string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); - if (File.Exists(target)) continue; - if (!downloadOnStart || modelEntry.url == null || modelEntry.url == "") copyCallback(modelEntry.path, target); - else downloads.Add(new StringPair { source = modelEntry.url, target = modelEntry.filename }); + modelEntriesBuild.Add(modelEntry.OnlyRequiredFields()); } + string json = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntriesBuild, downloadOnStart = downloadOnStart }, true); + File.WriteAllText(LLMUnitySetup.LLMManagerPath, json); + } + + public static void Build(ActionCallback copyCallback) + { + SaveToDisk(); - if (downloads.Count > 0) + foreach (ModelEntry modelEntry in modelEntries) { - string downloadJSON = JsonUtility.ToJson(new ListStringPair { pairs = downloads }, true); - using (FileStream fs = new FileStream(LLMUnitySetup.BuildFile, FileMode.Create, FileAccess.Write)) - { - using (BinaryWriter writer = new BinaryWriter(fs)) writer.Write(downloadJSON); - } + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (!modelEntry.includeInBuild || File.Exists(target)) continue; + if (!downloadOnStart || string.IsNullOrEmpty(modelEntry.url)) copyCallback(modelEntry.path, target); } } diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 62d087ee..0a0d7169 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -97,10 +97,8 @@ public class LLMUnitySetup public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); /// Temporary dir for build public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); - /// Name of file with build information for runtime - public static string BuildFilename = "LLMUnityBuild.bin"; /// Path of file with build information for runtime - public static string BuildFile = GetAssetPath(BuildFilename); + public static string LLMManagerPath = GetAssetPath("LLMManager.bin"); /// Default models for download [HideInInspector] public static readonly (string, string, string)[] modelOptions = new(string, string, string)[] @@ -277,6 +275,12 @@ public static async Task AndroidExtractFile(string assetName, bool overwrite = f } } + public static async Task AndroidExtractAsset(string path) + { + if (Application.platform != RuntimePlatform.Android) return; + await AndroidExtractFile(Path.GetFileName(path)); + } + public static bool IsSubPath(string childPath, string parentPath) { string fullParentPath = Path.GetFullPath(parentPath).Replace('\\', '/'); From 5d37e084e7a1621bb550cefee9ec2d0864af52c4 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:03:03 +0300 Subject: [PATCH 077/105] add more tests for SetModel --- Tests/Runtime/TestLLM.cs | 110 ++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index 0647f917..c56a591f 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -6,37 +6,52 @@ using System; using System.Collections; using UnityEngine.TestTools; +using System.IO; namespace LLMUnityTests { public class TestLLM { - GameObject gameObject; - LLM llm; - LLMCharacter llmCharacter; + protected GameObject gameObject; + protected LLM llm; + protected LLMCharacter llmCharacter; + protected static string modelUrl = "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"; + protected static string filename = Path.GetFileName(modelUrl).Split("?")[0]; Exception error = null; string prompt = "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request."; public TestLLM() { + LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.All); Task task = Init(); task.Wait(); } - public async Task Init() + public virtual async Task Init() { gameObject = new GameObject(); gameObject.SetActive(false); + await SetLLM(); + SetLLMCharacter(); + gameObject.SetActive(true); + } + public async Task EmptyTask() + { + await Task.Delay(1); + } + + public virtual async Task SetLLM() + { llm = gameObject.AddComponent(); - string modelUrl = "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"; - string modelPath = "LLMUnityTests/smol_llama-220m-openhermes.q4_k_m.gguf"; - string fullModelPath = LLMUnitySetup.GetAssetPath(modelPath); - await LLMUnitySetup.DownloadFile(modelUrl, fullModelPath, false, null, null); - llm.SetModel(fullModelPath); + string filename = await LLMManager.DownloadModel(modelUrl); + llm.SetModel(filename); llm.parallelPrompts = 1; llm.SetTemplate("alpaca"); + } + public virtual void SetLLMCharacter() + { llmCharacter = gameObject.AddComponent(); llmCharacter.llm = llm; llmCharacter.playerName = "Instruction"; @@ -46,11 +61,9 @@ public async Task Init() llmCharacter.seed = 0; llmCharacter.stream = false; llmCharacter.numPredict = 20; - - gameObject.SetActive(true); } - public async Task RunTests() + public virtual async Task RunTests() { error = null; try @@ -91,6 +104,7 @@ public IEnumerator RunTestsWait() Debug.LogError(error.ToString()); throw (error); } + OnDestroy(); } public void TestInitParameters(int nkeep, int chats) @@ -126,5 +140,77 @@ public void TestPostChat(int num) { Assert.That(llmCharacter.chat.Count == num); } + + public virtual void OnDestroy() + { + LLMManager.Remove(filename); + } + } + + public class TestLLM_LLMManager_Load : TestLLM + { + public override Task SetLLM() + { + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + filename = LLMManager.LoadModel(sourcePath); + llm.SetModel(filename); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } + } + + public class TestLLM_StreamingAssets_Load : TestLLM + { + public override Task SetLLM() + { + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + string targetPath = LLMUnitySetup.GetAssetPath(filename); + if (!File.Exists(targetPath)) File.Copy(sourcePath, targetPath); + llm.SetModel(filename); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } + + public override void OnDestroy() + { + string targetPath = LLMUnitySetup.GetAssetPath(filename); + if (!File.Exists(targetPath)) File.Delete(targetPath); + } + } + + public class TestLLM_SetModel_Fail : TestLLM + { + public TestLLM_SetModel_Fail() + { + LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.None); + Task task = Init(); + task.Wait(); + } + + public override Task SetLLM() + { + LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.None); + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + llm.SetModel(sourcePath); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } + + public override Task RunTests() + { + Assert.That(llm.model == ""); + llm.Awake(); + Assert.That(llm.failed); + llm.OnDestroy(); + return Task.CompletedTask; + } + + public override void OnDestroy() {} } } From 8e769c58afe79e7c4b18fb8652b91d76a357ab24 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:04:28 +0300 Subject: [PATCH 078/105] small adaptations --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d9c8dfb6..cb49d4cd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger ## At a glance -- 💻 Cross-platform! Windows, Linux and macOS +- 💻 Cross-platform! Windows, Linux, macOS and Android - 🏠 Runs locally without internet access. No data ever leaves the game! - ⚡ Blazing fast inference on CPU and GPU (Nvidia, AMD, Apple Metal) - 🤗 Supports all major LLM models @@ -79,7 +79,7 @@ First you will setup the LLM for your game 🏎: Then you can setup each of your characters as follows 🙋‍♀️: - Create an empty GameObject for the character.
In the GameObject Inspector click `Add Component` and select the LLMCharacter script. - Select the LLM constructed above in the `LLM` field. -- Define the role of your AI in the `Prompt`. You can also define the name of the AI (`AI Name`) and the player (`Player Name`). +- Define the role of your AI in the `Prompt`. You can define the name of the AI (`AI Name`) and the player (`Player Name`). You can also adjust the LLM and character settings according to your preference (see [Options](#options)). @@ -268,10 +268,10 @@ public class MyScript : MonoBehaviour
Use a remote server -You can also use a remote server that does the processing and implement Characters that interact with it. To do that: +You can also use a remote server that does the processing and implement characters that interact with it. To do that: - Create a project with a GameObject using the `LLM` script as described above. Enable the `Remote` option and optionally configure the port. - Create a second project with the game characters using the `LLMCharacter` script as described above. - Enable the `Remote` option and configure the host and port with the IP address (starting with "http://") and port of the server. + Enable the `Remote` option and configure the host with the IP address (starting with "http://") and port of the server.
@@ -298,11 +298,11 @@ You can also load your own model in .gguf format with the `Load model` button (s Save the scene, run and enjoy! ## Use your own model -LLM for Unity uses the [Mistral 7B Instruct](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2), [OpenHermes 2.5](https://huggingface.co/teknium/OpenHermes-2.5-Mistral-7B) or [Microsoft Phi-3](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf) model by default, quantised with the Q4 method.
+LLM for Unity has different state of the art models built-in for different model sizes, quantised with the Q4_K_M method.
Alternative models can be downloaded from [HuggingFace](https://huggingface.co/models?library=gguf&sort=downloads).
The required model format is .gguf as defined by the llama.cpp.
-HuggingFace models can be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
+HuggingFace models can alternatively be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
❕ Before using any model make sure you **check their license** ❕ From 4f5c0cc0f2e68dda2f6fda95a77223377ddd2944 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:04:52 +0300 Subject: [PATCH 079/105] renaming of waiting function --- Samples~/AndroidDemo/AndroidDemo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs index 58b166cd..92e52b7f 100644 --- a/Samples~/AndroidDemo/AndroidDemo.cs +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -29,7 +29,7 @@ async Task DownloadThenWarmup() { ChatPanel.SetActive(false); DownloadPanel.SetActive(true); - bool downloadOK = await LLM.WaitUntilModelsDownloaded(SetProgress); + bool downloadOK = await LLM.WaitUntilModelSetup(SetProgress); if (!downloadOK) { ErrorText.SetActive(true); From 2f0a3a931ec77068230c771795398e571fc53422 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:52:05 +0300 Subject: [PATCH 080/105] unregister in any case, setdirty for template, expose model variables --- Runtime/LLM.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 505bf24e..92ff228a 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -65,12 +65,12 @@ public class LLM : MonoBehaviour /// the LLM model to use. /// Models with .gguf format are allowed. - public string model = ""; + [ModelAdvanced] public string model = ""; /// Chat template used for the model - public string chatTemplate = ChatTemplate.DefaultTemplate; + [ModelAdvanced] public string chatTemplate = ChatTemplate.DefaultTemplate; /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). /// Models with .bin format are allowed. - public string lora = ""; + [ModelAdvanced] public string lora = ""; /// \cond HIDE @@ -211,6 +211,9 @@ public void SetTemplate(string templateName) { chatTemplate = templateName; if (started) llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); +#if UNITY_EDITOR + if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); +#endif } /// @@ -538,9 +541,7 @@ public void Destroy() public void OnDestroy() { Destroy(); -#if UNITY_EDITOR LLMManager.Unregister(this); -#endif } } } From 9e56cd19c95aae3b7baa3779c4a0e35899e3263f Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 00:52:28 +0300 Subject: [PATCH 081/105] set template only for non null llms --- Runtime/LLMManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 599f0e46..978dea4d 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -133,7 +133,7 @@ public static void SetTemplate(ModelEntry entry, string chatTemplate) entry.chatTemplate = chatTemplate; foreach (LLM llm in llms) { - if (llm.model == entry.filename) llm.SetTemplate(chatTemplate); + if (llm != null && llm.model == entry.filename) llm.SetTemplate(chatTemplate); } #if UNITY_EDITOR Save(); From 3c9e28c41f3fba4a71a70daaf8105ce81e40e101 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:44:11 +0300 Subject: [PATCH 082/105] smaller submit buttons --- Editor/LLMEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 72920d0e..a019ac40 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -166,8 +166,8 @@ async Task createCustomURLField() EditorGUILayout.LabelField("Enter URL", GUILayout.Width(100)); GUI.SetNextControlName("customURLFocus"); customURL = EditorGUILayout.TextField(customURL, GUILayout.Width(buttonWidth)); - submit = GUILayout.Button("Submit", GUILayout.Width(buttonWidth)); - exit = GUILayout.Button("Back", GUILayout.Width(buttonWidth)); + submit = GUILayout.Button("Submit", GUILayout.Width(buttonWidth / 2)); + exit = GUILayout.Button("Back", GUILayout.Width(buttonWidth / 2)); EditorGUILayout.EndHorizontal(); if (customURLFocus) From 145d3e0e4727a25246eaa326ab6e6a285c00d1ff Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:44:37 +0300 Subject: [PATCH 083/105] dont check isplaying from thread --- Runtime/LLM.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 92ff228a..18ef2ce9 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -207,12 +207,12 @@ public void SetLora(string path) /// Set the chat template for the LLM. /// /// the chat template to use. The available templates can be found in the ChatTemplate.templates.Keys array - public void SetTemplate(string templateName) + public void SetTemplate(string templateName, bool setDirty=true) { chatTemplate = templateName; if (started) llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); #if UNITY_EDITOR - if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); + if (setDirty && !EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif } @@ -324,7 +324,7 @@ private void InitServer(string arguments) if (debug) SetupLogging(); LLMObject = llmlib.LLM_Construct(arguments); if (remote) llmlib.LLM_StartServer(LLMObject); - SetTemplate(chatTemplate); + SetTemplate(chatTemplate, false); CheckLLMStatus(false); } From 836053e5a94cf110fdc46be93dfc9a6c3f6765f9 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:45:54 +0300 Subject: [PATCH 084/105] overwrite small files in android, extract only once --- Runtime/LLMCharacter.cs | 2 +- Runtime/LLMManager.cs | 2 +- Runtime/LLMUnitySetup.cs | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 8d35559b..9f37721a 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -324,7 +324,7 @@ public async void SetGrammar(string path) #if UNITY_EDITOR if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path); #endif - await LLMUnitySetup.AndroidExtractAsset(path); + await LLMUnitySetup.AndroidExtractAsset(path, true); grammar = path; InitGrammar(); } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 978dea4d..8caea3fc 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -77,7 +77,7 @@ public static Task Setup() public static async Task SetupOnce() { - await LLMUnitySetup.AndroidExtractAsset(LLMUnitySetup.LLMManagerPath); + await LLMUnitySetup.AndroidExtractAsset(LLMUnitySetup.LLMManagerPath, true); LoadFromDisk(); if (!downloadOnStart) return true; diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 0a0d7169..eb424a88 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -138,6 +138,8 @@ public enum DebugModeType } [LLMUnity] public static DebugModeType DebugMode = DebugModeType.All; static List> errorCallbacks = new List>(); + static readonly object lockObject = new object(); + static Dictionary androidExtractTasks = new Dictionary(); public static void Log(string message) { @@ -239,6 +241,20 @@ public static async Task DownloadFile( } public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024) + { + Task extractionTask; + lock (lockObject) + { + if (!androidExtractTasks.TryGetValue(assetName, out extractionTask)) + { + extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize); + androidExtractTasks[assetName] = extractionTask; + } + } + await extractionTask; + } + + public static async Task AndroidExtractFileOnce(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024) { string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName; string target = GetAssetPath(assetName); @@ -275,10 +291,10 @@ public static async Task AndroidExtractFile(string assetName, bool overwrite = f } } - public static async Task AndroidExtractAsset(string path) + public static async Task AndroidExtractAsset(string path, bool overwrite = false) { if (Application.platform != RuntimePlatform.Android) return; - await AndroidExtractFile(Path.GetFileName(path)); + await AndroidExtractFile(Path.GetFileName(path), overwrite); } public static bool IsSubPath(string childPath, string parentPath) From d982aa1709fd9398d39211ad15fffbcc94aa8a83 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:46:28 +0300 Subject: [PATCH 085/105] rename LLMManagerPath to json --- Runtime/LLMUnitySetup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index eb424a88..b2e1e617 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -98,7 +98,7 @@ public class LLMUnitySetup /// Temporary dir for build public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); /// Path of file with build information for runtime - public static string LLMManagerPath = GetAssetPath("LLMManager.bin"); + public static string LLMManagerPath = GetAssetPath("LLMManager.json"); /// Default models for download [HideInInspector] public static readonly (string, string, string)[] modelOptions = new(string, string, string)[] From d2f1fa29511280f3c38820c75f4a5bb5084657a7 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:55:18 +0300 Subject: [PATCH 086/105] update samples --- Samples~/AndroidDemo/Scene.unity | 17 ++---- Samples~/ChatBot/Scene.unity | 10 +--- Samples~/KnowledgeBaseGame/Scene.unity | 8 +-- Samples~/MultipleCharacters/Scene.unity | 10 +--- Samples~/SimpleInteraction/Scene.unity | 10 +--- Samples~/SimpleInteraction/TestDownload.cs | 59 +++++++++++++++++++ .../SimpleInteraction/TestDownload.cs.meta | 11 ++++ 7 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 Samples~/SimpleInteraction/TestDownload.cs create mode 100644 Samples~/SimpleInteraction/TestDownload.cs.meta diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity index 81f310b9..6e10a3c0 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -167,11 +167,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 708542fec6999ea3ebd7c69404932bb3, type: 3} m_Name: m_EditorClassIdentifier: - llm: {fileID: 1047848254} llmCharacter: {fileID: 498662973} ChatPanel: {fileID: 1084608230} playerText: {fileID: 1966107897} AIText: {fileID: 887085510} + ErrorText: {fileID: 0} DownloadPanel: {fileID: 332743750} progressBar: {fileID: 332743752} progressText: {fileID: 381203299} @@ -1323,24 +1323,17 @@ MonoBehaviour: advancedOptions: 0 remote: 0 port: 13333 - numThreads: 2 + numThreads: -1 numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: Phi-3-mini-4k-instruct-q4.gguf - downloadOnBuild: 1 - modelURL: https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true - lora: contextSize: 0 batchSize: 512 basePrompt: - SelectedModel: 3 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 - chatTemplate: phi-3 + model: + chatTemplate: chatml + lora: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/ChatBot/Scene.unity b/Samples~/ChatBot/Scene.unity index 07764b61..947f4a39 100644 --- a/Samples~/ChatBot/Scene.unity +++ b/Samples~/ChatBot/Scene.unity @@ -663,17 +663,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!1 &1051131186 GameObject: m_ObjectHideFlags: 0 diff --git a/Samples~/KnowledgeBaseGame/Scene.unity b/Samples~/KnowledgeBaseGame/Scene.unity index 3be84471..ff10abb9 100644 --- a/Samples~/KnowledgeBaseGame/Scene.unity +++ b/Samples~/KnowledgeBaseGame/Scene.unity @@ -7542,16 +7542,12 @@ MonoBehaviour: debug: 0 parallelPrompts: -1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 basePrompt: - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + model: chatTemplate: chatml + lora: --- !u!4 &2142407557 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/MultipleCharacters/Scene.unity b/Samples~/MultipleCharacters/Scene.unity index 999f08a1..75117583 100644 --- a/Samples~/MultipleCharacters/Scene.unity +++ b/Samples~/MultipleCharacters/Scene.unity @@ -1520,17 +1520,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/SimpleInteraction/Scene.unity b/Samples~/SimpleInteraction/Scene.unity index 9e41c779..b4e5e246 100644 --- a/Samples~/SimpleInteraction/Scene.unity +++ b/Samples~/SimpleInteraction/Scene.unity @@ -1043,17 +1043,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/SimpleInteraction/TestDownload.cs b/Samples~/SimpleInteraction/TestDownload.cs new file mode 100644 index 00000000..585814d1 --- /dev/null +++ b/Samples~/SimpleInteraction/TestDownload.cs @@ -0,0 +1,59 @@ +using UnityEngine; +using LLMUnity; +using UnityEngine.UI; +using System.Threading.Tasks; +using System.IO; + +namespace LLMUnitySamples +{ + public class TestDownload : MonoBehaviour + { + public InputField playerText; + public Scrollbar progressBar; + public Text progressText; + public Toggle overwriteToggle; + + void Start() + { + playerText.onSubmit.AddListener(onInputFieldSubmit); + } + + void SetProgress(float progress) + { + // Debug.Log(progress); + progressText.text = ((int)(progress * 100)).ToString() + "%"; + progressBar.size = progress; + } + + string path; + void onInputFieldSubmit(string message) + { + string url = message.Trim(); + path = "/tmp/" + Path.GetFileName(url).Split("?")[0]; + playerText.interactable = false; + Debug.Log(overwriteToggle.isOn); + _ = LLMUnitySetup.DownloadFile( + url, path, overwriteToggle.isOn, + CompleteCallback, SetProgress + ); + } + + public void CompleteCallback(string path) + { + Complete(); + } + + public void Complete() + { + playerText.interactable = true; + playerText.Select(); + playerText.text = ""; + } + + public void CancelRequests() + { + LLMUnitySetup.CancelDownload(path); + Complete(); + } + } +} diff --git a/Samples~/SimpleInteraction/TestDownload.cs.meta b/Samples~/SimpleInteraction/TestDownload.cs.meta new file mode 100644 index 00000000..5c4986c8 --- /dev/null +++ b/Samples~/SimpleInteraction/TestDownload.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca77155fbdc403fffad6c19155d4d894 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 3b29685249db2c2a047dfc7028bdc9a9f3de6ab2 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Fri, 2 Aug 2024 02:57:19 +0300 Subject: [PATCH 087/105] show stop button on start --- Samples~/ChatBot/ChatBot.cs | 3 +++ Samples~/ChatBot/Scene.unity | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Samples~/ChatBot/ChatBot.cs b/Samples~/ChatBot/ChatBot.cs index 0aeea759..3a2b1d77 100644 --- a/Samples~/ChatBot/ChatBot.cs +++ b/Samples~/ChatBot/ChatBot.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LLMUnity; +using UnityEngine.UI; namespace LLMUnitySamples { @@ -18,6 +19,7 @@ public class ChatBot : MonoBehaviour public float textPadding = 10f; public float bubbleSpacing = 10f; public Sprite sprite; + public Button stopButton; private InputBubble inputBubble; private List chatBubbles = new List(); @@ -51,6 +53,7 @@ void Start() inputBubble.AddSubmitListener(onInputFieldSubmit); inputBubble.AddValueChangedListener(onValueChanged); inputBubble.setInteractable(false); + stopButton.gameObject.SetActive(true); _ = llmCharacter.Warmup(WarmUpCallback); } diff --git a/Samples~/ChatBot/Scene.unity b/Samples~/ChatBot/Scene.unity index 947f4a39..bea05e22 100644 --- a/Samples~/ChatBot/Scene.unity +++ b/Samples~/ChatBot/Scene.unity @@ -182,6 +182,7 @@ MonoBehaviour: textPadding: 10 bubbleSpacing: 10 sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + stopButton: {fileID: 600245850} --- !u!1 &507661347 GameObject: m_ObjectHideFlags: 0 @@ -350,7 +351,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &600245849 RectTransform: m_ObjectHideFlags: 0 From 3a6db07d0d442746e2913bc6c146c49191139625 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Sun, 4 Aug 2024 11:37:26 +0300 Subject: [PATCH 088/105] SetModel allowing for both local and deployable models --- Runtime/LLM.cs | 39 +++++++++++++++++++-------------------- Runtime/LLMBuilder.cs | 8 +++++++- Runtime/LLMManager.cs | 40 +++++++++++++++++++++++++++++++++------- Tests/Runtime/TestLLM.cs | 21 +-------------------- 4 files changed, 60 insertions(+), 48 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 18ef2ce9..cb35ba0b 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -140,33 +140,32 @@ public static async Task WaitUntilModelSetup(Callback downloadProgr public string GetModelLoraPath(string path) { - string modelPath = LLMUnitySetup.GetAssetPath(path); -#if UNITY_EDITOR - if (!File.Exists(modelPath)) - { - ModelEntry modelEntry = LLMManager.Get(path); - if (modelEntry != null) modelPath = modelEntry.path; - } -#endif - return modelPath; + string assetPath = LLMManager.GetAssetPath(path); + if (!string.IsNullOrEmpty(assetPath)) return assetPath; + return path; } public string SetModelLoraPath(string path, bool lora) { ModelEntry modelEntry = LLMManager.Get(path); if (modelEntry != null) return modelEntry.filename; - string assetPath = LLMUnitySetup.GetAssetPath(path); - if (LLMUnitySetup.IsSubPath(assetPath, LLMUnitySetup.GetAssetPath()) && File.Exists(assetPath)) return path; - string errorMessage; string modelType = lora ? "Lora" : "Model"; - if (File.Exists(path)) errorMessage = $"The {modelType} path needs to be relative to the StreamingAssets folder."; - else errorMessage = $"The {modelType} path was not found."; - errorMessage += " Use one of the following methods:"; - errorMessage += $"\n-Copy the {modelType} inside the StreamingAssets folder and use its relative path or"; - errorMessage += $"\n-Load the {modelType} with the LLMManager: `string filename=LLMManager.Load{modelType}(path); llm.Set{modelType}(filename)`"; - LLMUnitySetup.LogError(errorMessage); - return ""; + string assetPath = LLMUnitySetup.GetAssetPath(path); + if (!File.Exists(assetPath)) + { + LLMUnitySetup.LogError($"The {modelType} file {path} was not found."); + return path; + } + + if (!LLMUnitySetup.IsSubPath(assetPath, LLMUnitySetup.GetAssetPath())) + { + string errorMessage = $"The {modelType} file {path} was loaded locally. If you want to include it in the build:"; + errorMessage += $"\n-Copy the {modelType} inside the StreamingAssets folder and use its relative path or"; + errorMessage += $"\n-Load the {modelType} with the LLMManager: `string filename=LLMManager.Load{modelType}(path); llm.Set{modelType}(filename)`"; + LLMUnitySetup.LogWarning(errorMessage); + } + return assetPath; } /// @@ -207,7 +206,7 @@ public void SetLora(string path) /// Set the chat template for the LLM. /// /// the chat template to use. The available templates can be found in the ChatTemplate.templates.Keys array - public void SetTemplate(string templateName, bool setDirty=true) + public void SetTemplate(string templateName, bool setDirty = true) { chatTemplate = templateName; if (started) llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs index 560d8901..10674adf 100644 --- a/Runtime/LLMBuilder.cs +++ b/Runtime/LLMBuilder.cs @@ -59,6 +59,12 @@ static void CopyActionAddMeta(string source, string target) AddTargetPair(target + ".meta"); } + static void AddActionAddMeta(string target) + { + AddTargetPair(target); + AddTargetPair(target + ".meta"); + } + static bool DeleteAction(string source) { return LLMUnitySetup.DeletePath(source); @@ -85,7 +91,7 @@ public static void HideLibraryPlatforms(string platform) public static void BuildModels() { LLMManager.Build(CopyActionAddMeta); - if (File.Exists(LLMUnitySetup.LLMManagerPath)) AddMovedPair("", LLMUnitySetup.LLMManagerPath); + if (File.Exists(LLMUnitySetup.LLMManagerPath)) AddActionAddMeta(LLMUnitySetup.LLMManagerPath); } public static void Reset() diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 8caea3fc..def6157d 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -140,15 +140,28 @@ public static void SetTemplate(ModelEntry entry, string chatTemplate) #endif } - public static ModelEntry Get(string filename) + public static ModelEntry Get(string path) { + string filename = Path.GetFileName(path); + string fullPath = Path.GetFullPath(path).Replace('\\', '/'); foreach (ModelEntry entry in modelEntries) { - if (entry.filename == filename) return entry; + if (entry.filename == filename || entry.path == fullPath) return entry; } return null; } + public static string GetAssetPath(string filename) + { + ModelEntry entry = Get(filename); + if (entry == null) return ""; +#if UNITY_EDITOR + return entry.path; +#else + return LLMUnitySetup.GetAssetPath(entry.filename); +#endif + } + public static int Num(bool lora) { int num = 0; @@ -227,9 +240,21 @@ public static async Task Download(string url, bool lora = false, string { foreach (ModelEntry entry in modelEntries) { - if (entry.url == url) return entry.filename; + if (entry.url == url) + { + LLMUnitySetup.Log($"Found existing entry for {url}"); + return entry.filename; + } } + string modelName = Path.GetFileName(url).Split("?")[0]; + ModelEntry entryPath = Get(modelName); + if (entryPath != null) + { + LLMUnitySetup.Log($"Found existing entry for {modelName}"); + return entryPath.filename; + } + string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); float preModelProgress = modelProgress; float preLoraProgress = loraProgress; @@ -258,12 +283,13 @@ public static async Task Download(string url, bool lora = false, string public static string Load(string path, bool lora = false, string label = null) { - string fullPath = Path.GetFullPath(path).Replace('\\', '/'); - foreach (ModelEntry entry in modelEntries) + ModelEntry entry = Get(path); + if (entry != null) { - if (entry.path == fullPath) return entry.filename; + LLMUnitySetup.Log($"Found existing entry for {entry.filename}"); + return entry.filename; } - return AddEntry(fullPath, lora, label); + return AddEntry(path, lora, label); } public static async Task DownloadModel(string url, string label = null) diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index c56a591f..b5afc880 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -182,18 +182,10 @@ public override void OnDestroy() } } - public class TestLLM_SetModel_Fail : TestLLM + public class TestLLM_SetModel_Warning : TestLLM { - public TestLLM_SetModel_Fail() - { - LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.None); - Task task = Init(); - task.Wait(); - } - public override Task SetLLM() { - LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.None); llm = gameObject.AddComponent(); string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); llm.SetModel(sourcePath); @@ -201,16 +193,5 @@ public override Task SetLLM() llm.SetTemplate("alpaca"); return Task.CompletedTask; } - - public override Task RunTests() - { - Assert.That(llm.model == ""); - llm.Awake(); - Assert.That(llm.failed); - llm.OnDestroy(); - return Task.CompletedTask; - } - - public override void OnDestroy() {} } } From 77a9b6eef867b0897210a9a1a3a823ce0b5a87d6 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 11:44:16 +0300 Subject: [PATCH 089/105] move copy/move path functionality to builder --- Runtime/LLMBuilder.cs | 63 ++++++++++++++++++++++++++++++++-------- Runtime/LLMUnitySetup.cs | 39 ------------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs index 10674adf..a3cf0070 100644 --- a/Runtime/LLMBuilder.cs +++ b/Runtime/LLMBuilder.cs @@ -9,15 +9,52 @@ namespace LLMUnity public class LLMBuilder { static List movedPairs = new List(); - static string movedCache = Path.Combine(LLMUnitySetup.BuildTempDir, "moved.json"); + public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); + static string movedCache = Path.Combine(BuildTempDir, "moved.json"); [InitializeOnLoadMethod] private static void InitializeOnLoad() { - Directory.CreateDirectory(LLMUnitySetup.BuildTempDir); Reset(); } + public static void CopyPath(string source, string target) + { + if (File.Exists(source)) + { + File.Copy(source, target, true); + } + else if (Directory.Exists(source)) + { + Directory.CreateDirectory(target); + List filesAndDirs = new List(); + filesAndDirs.AddRange(Directory.GetFiles(source)); + filesAndDirs.AddRange(Directory.GetDirectories(source)); + foreach (string path in filesAndDirs) + { + CopyPath(path, Path.Combine(target, Path.GetFileName(path))); + } + } + } + + public static void MovePath(string source, string target) + { + CopyPath(source, target); + DeletePath(source); + } + + public static bool DeletePath(string path) + { + if (!LLMUnitySetup.IsSubPath(path, LLMUnitySetup.GetAssetPath()) && !LLMUnitySetup.IsSubPath(path, BuildTempDir)) + { + LLMUnitySetup.LogError($"Safeguard: {path} will not be deleted because it may not be safe"); + return false; + } + if (File.Exists(path)) File.Delete(path); + else if (Directory.Exists(path)) Directory.Delete(path, true); + return true; + } + static void AddMovedPair(string source, string target) { movedPairs.Add(new StringPair {source = source, target = target}); @@ -33,7 +70,7 @@ static bool MoveAction(string source, string target, bool addEntry = true) { ActionCallback moveCallback; if (File.Exists(source)) moveCallback = File.Move; - else if (Directory.Exists(source)) moveCallback = LLMUnitySetup.MovePath; + else if (Directory.Exists(source)) moveCallback = MovePath; else return false; if (addEntry) AddMovedPair(source, target); @@ -45,7 +82,7 @@ static bool CopyAction(string source, string target, bool addEntry = true) { ActionCallback copyCallback; if (File.Exists(source)) copyCallback = File.Copy; - else if (Directory.Exists(source)) copyCallback = LLMUnitySetup.CopyPath; + else if (Directory.Exists(source)) copyCallback = CopyPath; else return false; if (addEntry) AddTargetPair(target); @@ -65,11 +102,6 @@ static void AddActionAddMeta(string target) AddTargetPair(target + ".meta"); } - static bool DeleteAction(string source) - { - return LLMUnitySetup.DeletePath(source); - } - public static void HideLibraryPlatforms(string platform) { List platforms = new List(){ "windows", "macos", "linux", "android", "ios" }; @@ -80,7 +112,7 @@ public static void HideLibraryPlatforms(string platform) { if (Path.GetFileName(source).StartsWith(platformPrefix)) { - string target = Path.Combine(LLMUnitySetup.BuildTempDir, Path.GetFileName(source)); + string target = Path.Combine(BuildTempDir, Path.GetFileName(source)); MoveAction(source, target); MoveAction(source + ".meta", target + ".meta"); } @@ -94,6 +126,13 @@ public static void BuildModels() if (File.Exists(LLMUnitySetup.LLMManagerPath)) AddActionAddMeta(LLMUnitySetup.LLMManagerPath); } + public static void Build(string platform) + { + Directory.CreateDirectory(BuildTempDir); + HideLibraryPlatforms(platform); + BuildModels(); + } + public static void Reset() { if (!File.Exists(movedCache)) return; @@ -103,11 +142,11 @@ public static void Reset() bool refresh = false; foreach (var pair in movedPairs) { - if (pair.source == "") refresh |= DeleteAction(pair.target); + if (pair.source == "") refresh |= DeletePath(pair.target); else refresh |= MoveAction(pair.target, pair.source, false); } if (refresh) AssetDatabase.Refresh(); - LLMUnitySetup.DeletePath(movedCache); + DeletePath(movedCache); } } } diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index b2e1e617..58c6b677 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -95,8 +95,6 @@ public class LLMUnitySetup public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); /// Model download path public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); - /// Temporary dir for build - public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); /// Path of file with build information for runtime public static string LLMManagerPath = GetAssetPath("LLMManager.json"); @@ -306,43 +304,6 @@ public static bool IsSubPath(string childPath, string parentPath) #if UNITY_EDITOR - public static void CopyPath(string source, string target) - { - if (File.Exists(source)) - { - File.Copy(source, target); - } - else if (Directory.Exists(source)) - { - Directory.CreateDirectory(target); - List filesAndDirs = new List(); - filesAndDirs.AddRange(Directory.GetFiles(source)); - filesAndDirs.AddRange(Directory.GetDirectories(source)); - foreach (string path in filesAndDirs) - { - CopyPath(path, Path.Combine(target, Path.GetFileName(path))); - } - } - } - - public static void MovePath(string source, string target) - { - CopyPath(source, target); - DeletePath(source); - } - - public static bool DeletePath(string path) - { - if (!IsSubPath(path, GetAssetPath()) && !IsSubPath(path, BuildTempDir)) - { - LogError($"Safeguard: {path} will not be deleted because it may not be safe"); - return false; - } - if (File.Exists(path)) File.Delete(path); - else if (Directory.Exists(path)) Directory.Delete(path, true); - return true; - } - [HideInInspector] public static float libraryProgress = 1; private static async Task DownloadLibrary() From e33a68d4dc6a80317ce12f5b12996207a50af0ff Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 11:45:25 +0300 Subject: [PATCH 090/105] log only on Editor by default --- Editor/LLMEditor.cs | 6 +++--- Runtime/LLMManager.cs | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index a019ac40..66d0825c 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -184,7 +184,7 @@ async Task createCustomURLField() Repaint(); if (submit && customURL != "") { - string filename = await LLMManager.Download(customURL, customURLLora); + string filename = await LLMManager.Download(customURL, customURLLora, true); SetModelIfNone(filename, customURLLora); UpdateModels(true); } @@ -206,7 +206,7 @@ async Task createButtons() else if (modelIndex > 1) { if (modelLicenses[modelIndex] != null) Debug.LogWarning($"The {modelOptions[modelIndex]} model is released under the following license: {modelLicenses[modelIndex]}. By using this model, you agree to the terms of the license."); - string filename = await LLMManager.DownloadModel(modelURLs[modelIndex], modelOptions[modelIndex]); + string filename = await LLMManager.DownloadModel(modelURLs[modelIndex], true, modelOptions[modelIndex]); SetModelIfNone(filename, false); UpdateModels(true); } @@ -218,7 +218,7 @@ async Task createButtons() string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); if (!string.IsNullOrEmpty(path)) { - string filename = LLMManager.LoadModel(path); + string filename = LLMManager.LoadModel(path, true); SetModelIfNone(filename, false); UpdateModels(); } diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index def6157d..1ba73ca0 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -236,13 +236,13 @@ public static string AddEntry(string path, bool lora = false, string label = nul return AddEntry(new ModelEntry(path, lora, label, url)); } - public static async Task Download(string url, bool lora = false, string label = null) + public static async Task Download(string url, bool lora = false, bool log = false, string label = null) { foreach (ModelEntry entry in modelEntries) { if (entry.url == url) { - LLMUnitySetup.Log($"Found existing entry for {url}"); + if (log) LLMUnitySetup.Log($"Found existing entry for {url}"); return entry.filename; } } @@ -251,7 +251,7 @@ public static async Task Download(string url, bool lora = false, string ModelEntry entryPath = Get(modelName); if (entryPath != null) { - LLMUnitySetup.Log($"Found existing entry for {modelName}"); + if (log) LLMUnitySetup.Log($"Found existing entry for {modelName}"); return entryPath.filename; } @@ -281,35 +281,35 @@ public static async Task Download(string url, bool lora = false, string return AddEntry(modelPath, lora, label, url); } - public static string Load(string path, bool lora = false, string label = null) + public static string Load(string path, bool lora = false, bool log = false, string label = null) { ModelEntry entry = Get(path); if (entry != null) { - LLMUnitySetup.Log($"Found existing entry for {entry.filename}"); + if (log) LLMUnitySetup.Log($"Found existing entry for {entry.filename}"); return entry.filename; } return AddEntry(path, lora, label); } - public static async Task DownloadModel(string url, string label = null) + public static async Task DownloadModel(string url, bool log = false, string label = null) { - return await Download(url, false, label); + return await Download(url, false, log, label); } - public static async Task DownloadLora(string url, string label = null) + public static async Task DownloadLora(string url, bool log = false, string label = null) { - return await Download(url, true, label); + return await Download(url, true, log, label); } - public static string LoadModel(string path, string label = null) + public static string LoadModel(string path, bool log = false, string label = null) { - return Load(path, false, label); + return Load(path, false, log, label); } - public static string LoadLora(string path, string label = null) + public static string LoadLora(string path, bool log = false, string label = null) { - return Load(path, true, label); + return Load(path, true, log, label); } public static void SetURL(string filename, string url) From 79573d8b1f305a430d753d8e79dd7bffe1ba4970 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 11:45:51 +0300 Subject: [PATCH 091/105] set earlier execution time --- Runtime/LLMManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 1ba73ca0..34ba5671 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -46,6 +46,7 @@ public class LLMManagerStore public List modelEntries; } + [DefaultExecutionOrder(-2)] public class LLMManager { public static bool downloadOnStart = false; From 7bee11b7139ea1571727ee53af9b07f117a97727 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 12:11:28 +0300 Subject: [PATCH 092/105] load lora through LLMManager --- Editor/LLMEditor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 66d0825c..87d79d7f 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -240,7 +240,9 @@ async Task createButtons() string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); if (!string.IsNullOrEmpty(path)) { - llmScript.SetLora(path); + string filename = LLMManager.LoadLora(path, true); + SetModelIfNone(filename, true); + UpdateModels(); } }; } From e8c69b9fbf44d5402596021c59db30931ceb5983 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 12:12:03 +0300 Subject: [PATCH 093/105] add model on top of manager if all loras --- Runtime/LLMManager.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index 34ba5671..ee1d91e8 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -218,12 +218,16 @@ public static string AddEntry(ModelEntry entry) int indexToInsert = modelEntries.Count; if (!entry.lora) { - for (int i = modelEntries.Count - 1; i >= 0; i--) + if (modelEntries[0].lora) indexToInsert = 0; + else { - if (!modelEntries[i].lora) + for (int i = modelEntries.Count - 1; i >= 0; i--) { - indexToInsert = i + 1; - break; + if (!modelEntries[i].lora) + { + indexToInsert = i + 1; + break; + } } } } From eda1fc4af577189379e26878ce4161c6900007fb Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Wed, 7 Aug 2024 12:37:53 +0300 Subject: [PATCH 094/105] download if url not empty - otherwise moel is copied --- Runtime/LLMManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index ee1d91e8..be69b07c 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -86,7 +86,7 @@ public static async Task SetupOnce() foreach (ModelEntry modelEntry in modelEntries) { string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); - if (!File.Exists(target)) downloads.Add(new StringPair {source = modelEntry.url, target = target}); + if (!File.Exists(target) && !string.IsNullOrEmpty(modelEntry.url)) downloads.Add(new StringPair {source = modelEntry.url, target = target}); } if (downloads.Count == 0) return true; From 1a68246e2254d17f0917f80bf78f5a13e2f3586e Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 13:06:49 +0300 Subject: [PATCH 095/105] move android setup logic to LLMManager --- Runtime/LLM.cs | 10 ---------- Runtime/LLMManager.cs | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index cb35ba0b..6a7adc3c 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -105,7 +105,6 @@ public async void Awake() failed = true; return; } - await AndroidSetup(); string arguments = GetLlamaccpArguments(); if (arguments == null) { @@ -117,15 +116,6 @@ public async void Awake() if (basePrompt != "") await SetBasePrompt(basePrompt); } - public async Task AndroidSetup() - { - if (Application.platform != RuntimePlatform.Android) return; - foreach (string path in new string[] {model, lora}) - { - if (path != "" && !File.Exists(LLMUnitySetup.GetAssetPath(path))) await LLMUnitySetup.AndroidExtractFile(path); - } - } - public async Task WaitUntilReady() { while (!started) await Task.Yield(); diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index be69b07c..b80be2c7 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -80,13 +80,22 @@ public static async Task SetupOnce() { await LLMUnitySetup.AndroidExtractAsset(LLMUnitySetup.LLMManagerPath, true); LoadFromDisk(); - if (!downloadOnStart) return true; List downloads = new List(); foreach (ModelEntry modelEntry in modelEntries) { string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); - if (!File.Exists(target) && !string.IsNullOrEmpty(modelEntry.url)) downloads.Add(new StringPair {source = modelEntry.url, target = target}); + if (File.Exists(target)) continue; + + if (!downloadOnStart || string.IsNullOrEmpty(modelEntry.url)) + { + await LLMUnitySetup.AndroidExtractFile(modelEntry.filename); + if (!File.Exists(target)) LLMUnitySetup.LogError($"Model {modelEntry.filename} could not be found!"); + } + else + { + downloads.Add(new StringPair {source = modelEntry.url, target = target}); + } } if (downloads.Count == 0) return true; @@ -109,6 +118,7 @@ public static async Task SetupOnce() { currFileSize = fileSizes[pair.source]; await LLMUnitySetup.DownloadFile(pair.source, pair.target, false, null, SetDownloadProgress); + await LLMUnitySetup.AndroidExtractFile(Path.GetFileName(pair.target)); completedSize += currFileSize; } From 98e204edaedcc19fb487a07342c336f7e9a0302c Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 13:07:18 +0300 Subject: [PATCH 096/105] fix lora model placement for empty list --- Runtime/LLMManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs index b80be2c7..ff1101d8 100644 --- a/Runtime/LLMManager.cs +++ b/Runtime/LLMManager.cs @@ -228,7 +228,7 @@ public static string AddEntry(ModelEntry entry) int indexToInsert = modelEntries.Count; if (!entry.lora) { - if (modelEntries[0].lora) indexToInsert = 0; + if (modelEntries.Count > 0 && modelEntries[0].lora) indexToInsert = 0; else { for (int i = modelEntries.Count - 1; i >= 0; i--) From 07c2a0fa76b27b992b4b753769c84a85b1776375 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 13:08:11 +0300 Subject: [PATCH 097/105] allow empty paths to disable model --- Runtime/LLM.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 6a7adc3c..2f4c57cf 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -137,6 +137,7 @@ public string GetModelLoraPath(string path) public string SetModelLoraPath(string path, bool lora) { + if (string.IsNullOrEmpty(path)) return path; ModelEntry modelEntry = LLMManager.Get(path); if (modelEntry != null) return modelEntry.filename; From fc2c644eb66c129fa650b02707aca35c26324897 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 13:34:11 +0300 Subject: [PATCH 098/105] add error text --- Samples~/AndroidDemo/Scene.unity | 92 +++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity index 6e10a3c0..d7a58ae7 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -171,7 +171,7 @@ MonoBehaviour: ChatPanel: {fileID: 1084608230} playerText: {fileID: 1966107897} AIText: {fileID: 887085510} - ErrorText: {fileID: 0} + ErrorText: {fileID: 1688602496} DownloadPanel: {fileID: 332743750} progressBar: {fileID: 332743752} progressText: {fileID: 381203299} @@ -386,6 +386,7 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: + - {fileID: 2047786417} - {fileID: 1688602497} - {fileID: 1705264488} - {fileID: 381203298} @@ -1850,12 +1851,12 @@ GameObject: - component: {fileID: 1688602499} - component: {fileID: 1688602498} m_Layer: 5 - m_Name: Title + m_Name: NetworkError m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &1688602497 RectTransform: m_ObjectHideFlags: 0 @@ -1872,7 +1873,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: -0.000025749, y: 15.999993} + m_AnchoredPosition: {x: -0.000025749, y: -28.3} m_SizeDelta: {x: 160, y: 30} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1688602498 @@ -1888,7 +1889,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 0.8983897, g: 0, b: 0, a: 1} m_RaycastTarget: 1 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 @@ -1908,7 +1909,7 @@ MonoBehaviour: m_HorizontalOverflow: 0 m_VerticalOverflow: 0 m_LineSpacing: 1 - m_Text: Downloading model... + m_Text: Network error occured --- !u!222 &1688602499 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2167,6 +2168,85 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2047786416 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2047786417} + - component: {fileID: 2047786419} + - component: {fileID: 2047786418} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2047786417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -0.000025749, y: 15.999993} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2047786418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Downloading model... +--- !u!222 &2047786419 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_CullTransparentMesh: 1 --- !u!1 &2091685446 GameObject: m_ObjectHideFlags: 0 From d8e46f0d795a71f293458576b848b0ecfe3df29c Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 13:43:23 +0300 Subject: [PATCH 099/105] update images --- .github/LLMCharacter_GameObject.png | Bin 35889 -> 33163 bytes .github/LLM_GameObject.png | Bin 28148 -> 33247 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/LLMCharacter_GameObject.png b/.github/LLMCharacter_GameObject.png index 59ead911faa55c135a35d93e7af8029c06caa376..1cba47a42df59871194879010399ba64d683eb0c 100644 GIT binary patch literal 33163 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 LLCrLElS}^v)z7Bb diff --git a/.github/LLM_GameObject.png b/.github/LLM_GameObject.png index 696c708627df59976b8439862cc6b4c787813d76..91391e8a8d203efbe9450c17a9bc128ffa6d7340 100644 GIT binary patch literal 33247 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

EgxDP_nCT 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 literal 28148 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 z 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 LpMKnF{!jk_?%C0a From f14cbd4a004b34c39188fc83d85233db72d29bd3 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 16:33:27 +0300 Subject: [PATCH 100/105] copy the model name from the copy menu --- Editor/LLMEditor.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 87d79d7f..bebaf578 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -305,7 +305,7 @@ void OnEnable() else if (!newSelected && isSelected) llmScript.SetLora(""); } - DrawCopyableLabel(nameRect, entry.label); + DrawCopyableLabel(nameRect, entry.label, entry.filename); if (!entry.lora) { @@ -376,9 +376,10 @@ void OnEnable() }; } - private void DrawCopyableLabel(Rect rect, string text) + private void DrawCopyableLabel(Rect rect, string label, string text = "") { - EditorGUI.LabelField(rect, text); + if (text == "") text = label; + EditorGUI.LabelField(rect, label); if (Event.current.type == EventType.ContextClick && rect.Contains(Event.current.mousePosition)) { GenericMenu menu = new GenericMenu(); From e48c716395dbb65816edede902e17e79c5d7f1f1 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 16:38:52 +0300 Subject: [PATCH 101/105] add LLM manager and Android info --- .github/LLM_manager.png | Bin 0 -> 20591 bytes .github/LLM_manager_expanded.png | Bin 0 -> 27398 bytes .github/android.png | Bin 0 -> 396520 bytes README.md | 86 ++++++++++++++++++++++--------- 4 files changed, 61 insertions(+), 25 deletions(-) create mode 100644 .github/LLM_manager.png create mode 100644 .github/LLM_manager_expanded.png create mode 100644 .github/android.png diff --git a/.github/LLM_manager.png b/.github/LLM_manager.png new file mode 100644 index 0000000000000000000000000000000000000000..5dbcb82909dd834eea67c0c5ab5d6b50a9b28173 GIT binary patch literal 20591 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 Fe*hkhi&+2w literal 0 HcmV?d00001 diff --git a/.github/LLM_manager_expanded.png b/.github/LLM_manager_expanded.png new file mode 100644 index 0000000000000000000000000000000000000000..1b6bde231f9a9ed8ec149186ef99e23d84e1698d GIT binary patch literal 27398 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 vh(vZEFf|ASDNoIBfa`&F|j&z2E#7WdCSetup  •  How to use  •  Examples  •  -Use your own model  •  +LLM model management  •  Options  •  License @@ -74,12 +74,12 @@ _Method 2: Install using the GitHub repo:_ First you will setup the LLM for your game 🏎: - Create an empty GameObject.
In the GameObject Inspector click `Add Component` and select the LLM script. -- Download one of the default models with the `Download Model` button (~GBs).
Or load your own .gguf model with the `Load model` button (see [Use your own model](#use-your-own-model)). +- Download one of the default models with the `Download Model` button (~GBs).
Or load your own .gguf model with the `Load model` button (see [LLM model management](#llm-model-management)). Then you can setup each of your characters as follows 🙋‍♀️: - Create an empty GameObject for the character.
In the GameObject Inspector click `Add Component` and select the LLMCharacter script. -- Select the LLM constructed above in the `LLM` field. - Define the role of your AI in the `Prompt`. You can define the name of the AI (`AI Name`) and the player (`Player Name`). +- (Optional) Select the LLM constructed above in the `LLM` field if you have more than one LLM GameObjects. You can also adjust the LLM and character settings according to your preference (see [Options](#options)). @@ -132,7 +132,17 @@ That's all ✨!

You can also: +

+Build a mobile app on Android + +To build an Android app you need to specify the `IL2CPP` scripting backend and the `ARM64` as the target architecture in the player settings.
+These settings can be accessed from the `Edit > Project Settings` menu within the `Player > Other Settings` section.
+ + +It is also a good idea to enable the `Download on Build` option in the LLM GameObject to download the model on launch in order to keep the app size small. + +
Save / Load your chat history @@ -157,7 +167,7 @@ where filename the filename or relative path of your choice. ``` c# void WarmupCompleted(){ // do something when the warmup is complete - Debug.Log("The AI is warm"); + Debug.Log("The AI is nice and ready"); } void Game(){ @@ -228,15 +238,20 @@ public class MyScript : MonoBehaviour async void Start() { - // disable gameObject so that Awake is not called immediately + // disable gameObject so that theAwake is not called immediately gameObject.SetActive(false); // Add an LLM object llm = gameObject.AddComponent(); - // set the model with a path relative to StreamingAssets folder - await llm.SetModel("Phi-3-mini-4k-instruct-q4.gguf"); - // you can also set a lora in a similar fashion - // await llm.SetLora("my-lora.bin"); + // set the model using the filename of the model. + // The model needs to be added to the LLM model manager (see LLM model management) by loading or downloading it. + // Otherwise the model file can be copied directly inside the StreamingAssets folder. + llm.SetModel("Phi-3-mini-4k-instruct-q4.gguf"); + // optional: you can also set a lora in a similar fashion + llm.SetLora("my-lora.bin"); + // optional: you can set the chat template of the model if it is not correctly identified + // You can find a list of chat templates in the ChatTemplate.templates.Keys + llm.SetTemplate("phi-3"); // optional: set number of threads llm.numThreads = -1; // optional: enable GPU by setting the number of model layers to offload to it @@ -251,12 +266,14 @@ public class MyScript : MonoBehaviour // set the AI and player name llmCharacter.AIName = "AI"; llmCharacter.playerName = "Human"; - // optional: set streaming to false to get the complete result in one go + // optional: set streaming to false to get the complete result in one go // llmCharacter.stream = true; - // optional: set a save path + // optional: set a save path // llmCharacter.save = "AICharacter1"; - // optional: set a grammar - // llmCharacter.SetGrammar("json.gbnf"); + // optional: enable the save cache to avoid recomputation when loading a save file (requires ~100 MB) + // llmCharacter.saveCache = true; + // optional: set a grammar + // await llmCharacter.SetGrammar("json.gbnf"); // re-enable gameObject gameObject.SetActive(true); @@ -268,7 +285,7 @@ public class MyScript : MonoBehaviour
Use a remote server -You can also use a remote server that does the processing and implement characters that interact with it. To do that: +You can use a remote server to carry out the processing and implement characters that interact with it. To do that: - Create a project with a GameObject using the `LLM` script as described above. Enable the `Remote` option and optionally configure the port. - Create a second project with the game characters using the `LLMCharacter` script as described above. Enable the `Remote` option and configure the host with the IP address (starting with "http://") and port of the server. @@ -293,16 +310,32 @@ To install a sample: - Select the `LLM for Unity` Package. From the `Samples` Tab, click `Import` next to the sample you want to install. The samples can be run with the `Scene.unity` scene they contain inside their folder.
-In the scene, select the `LLM` GameObject and click the `Download Model` button to download the default model.
-You can also load your own model in .gguf format with the `Load model` button (see [Use your own model](#use-your-own-model)).
+In the scene, select the `LLM` GameObject and click the `Download Model` button to download a default model or `Load model` to load your own model (see [LLM model management](#llm-model-management)).
Save the scene, run and enjoy! -## Use your own model -LLM for Unity has different state of the art models built-in for different model sizes, quantised with the Q4_K_M method.
- -Alternative models can be downloaded from [HuggingFace](https://huggingface.co/models?library=gguf&sort=downloads).
-The required model format is .gguf as defined by the llama.cpp.
-HuggingFace models can alternatively be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
+## LLM model management +LLM for Unity implements a model manager that allows to load or download LLMs and ship them directly in your game.
+The model manager can be found as part of the LLM GameObject:
+ + +You can download models with the `Download model` button.
+LLM for Unity includes different state of the art models built-in for different model sizes, quantised with the Q4_K_M method.
+Alternative models can be downloaded from [HuggingFace](https://huggingface.co/models?library=gguf&sort=downloads) in the .gguf format.
+You can download a model locally and load it with the `Load model` button, or copy the URL in the `Download model > Custom URL` field to directly download it.
+If a HuggingFace model does not provide a gguf file, it can be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
+ +The chat template used for constructing the prompts is determined automatically from the model (if a relevant entry exists) or the model name.
+If incorrecly identified, you can select another template from the chat template dropdown.
+
+Models added in the model manager are copied to the game during the building process.
+You can omit a model from being built in by deselecting the "Build" checkbox.
+To remove the model (but not delete it from disk) you can click the bin button.
+The the path and URL (if downloaded) of each added model is diplayed in the expanded view of the model manager access with the `>>` button:
+ + +You can create lighter builds by selecting the `Download on Build` option.
+Using this option the models will be downloaded the first time the game starts instead of copied in the build.
+If you have loaded a model locally you need to set its URL through the expanded view, otherwise it will be copied in the build.
❕ Before using any model make sure you **check their license** ❕ @@ -337,14 +370,17 @@ If the user's GPU is not supported, the LLM will fall back to the CPU #### 🤗 Model Settings - `Download model` click to download one of the default models - `Load model` click to load your own model in .gguf format -- `Model` the path of the model being used (relative to the Assets/StreamingAssets folder) --
Chat Template the chat template to use for constructing the prompts The chat template is determined automatically by the chat template of the model (if it exists) or the model name.
The "chatml" template works with most of the models.
+- `Download on Start` enable to downloaded the LLM models the first time the game starts. Alternatively the LLM models wil be copied directly in the build + -
Advanced options + - `Download lora` click to download a LoRA model in .bin format - `Load lora` click to load a LoRA model in .bin format - - `Lora` the path of the LoRA being used (relative to the Assets/StreamingAssets folder) -
Context Size size of the prompt context (0 = context size of the model) This is the number of tokens the model can take as input when generating responses. Higher values use more RAM or VRAM (if using GPU).
- `Batch Size` batch size for prompt processing (default: 512) + - `Model` the path of the model being used (relative to the Assets/StreamingAssets folder) + - `Chat Template` the chat template being used for the LLM + - `Lora` the path of the LoRA being used (relative to the Assets/StreamingAssets folder)
From 28ab31b2d8b5bc649be54438075584a4793aa4e0 Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 16:44:36 +0300 Subject: [PATCH 102/105] update changelogs --- CHANGELOG.md | 5 +++-- CHANGELOG.release.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3faf8051..8da18137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ #### 🚀 Features - Android deployment (PR: #194) -- LLM model selector with download store (PR: #196) -- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Allow to download models on startup with resumable download functionality (PR: #196) +- LLM model manager (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) - Start LLM always asynchronously (PR: #199) diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index d658bcea..6bf38b7d 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -1,7 +1,8 @@ ### 🚀 Features - Android deployment (PR: #194) -- LLM model selector with download store (PR: #196) -- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Allow to download models on startup with resumable download functionality (PR: #196) +- LLM model manager (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) - Start LLM always asynchronously (PR: #199) From 3d31b544930332278ac6d04a1384d2e3eb88fea0 Mon Sep 17 00:00:00 2001 From: Lorenzo Toniazzi Date: Sat, 27 Jul 2024 23:44:27 +0100 Subject: [PATCH 103/105] Add contributing guidelines --- CODE_OF_CONDUCT.md | 74 ++++++++++++++++++++++ CODE_OF_CONDUCT.md.meta | 7 +++ CONTRIBUTING.md | 133 ++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md.meta | 7 +++ 4 files changed, 221 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CODE_OF_CONDUCT.md.meta create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING.md.meta diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..c0a62ae6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project team member. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md.meta b/CODE_OF_CONDUCT.md.meta new file mode 100644 index 00000000..927b2541 --- /dev/null +++ b/CODE_OF_CONDUCT.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: df335783a8d8a4e53bbe626c147d010f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..33fa04c1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing to LLMUnity + +:+1: :tada: :heart: Thanks for your interest! :heart: :tada: :+1: + +The following is a set of guidelines for contributing to [LLMUnity](https://github.com/undreamai/LLMUnity). These are just guidelines, not rules. Use your best judgment, and +feel free to propose changes to this document in a pull request. + +#### Table Of Contents + +[How Can I Contribute?](#how-can-i-contribute) + * [Code of Conduct](#code-of-conduct) + * [Set up your dev environment](#set-up-your-dev-environment) + * [Reporting Bugs](#reporting-bugs) + * [Suggesting Enhancements](#suggesting-enhancements) + * [Good First Issue](#good-first-issue) + * [Issue and Pull Request Labels](#issue-and-pull-request-labels) + + +## How Can I Contribute? + +### Code of Conduct + +This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). +By participating, you are expected to uphold this code. + +### Set up your dev environment + + +1. Fork the repo. +2. Clone your forked repo into a Unity project's `Assets`. +3. Create a symbolic link to `Samples~`, for example with: + ```bash + cd Assets && ln -s ./LLMUnity/Samples~ ./Samples + ``` +4. Add the package to your projects libraries `Packages/manifest.json`: + ```json + "ai.undream.llm": "file:path/to/project/Assets/LLMUnity", + ``` +5. Create a topic branch from where you want to base your work. +Name your branch prefixed with an appropriate [label](https://github.com/undreamai/LLMUnity/labels), following the naming convention `enhancement/*`, `bug/*`, `documentation/*`, etc. Make commits of logical units. +6. Set up pre-commit hooks with `sh ./.github/setup.sh` + + +### Reporting Bugs + +This section guides you through submitting a bug report for LLMUnity. +Following these guidelines helps maintainers and the community understand your +report :pencil:, reproduce the behavior :computer:, and find related +reports :mag_right:. + +Before creating bug reports, please check [this section](#before-submitting-a-bug-report) +as you might find out that you don't need to create one. When you are creating +a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report) as it helps us resolve issues faster. + +#### Before Submitting A Bug Report + +**Perform a [cursory search](https://github.com/undreamai/LLMUnity/labels/bug)** +to see if the problem has already been reported. If it does exist, add a +[reaction](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/#reacting-to-ideas-in-issues-and-pull-requests) +to the issue to indicate this is also an issue for you, and add a +comment to the existing issue if there is extra information you can contribute. + +#### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). + +Simply create an issue on the [LLMUnity issue tracker](https://github.com/undreamai/LLMUnity/issues), choose the appropriate provided issue template and fill it out. + +The information we are interested in includes: + + - details about your environment - which build, which operating system + - details about reproducing the issue - what steps to take, what happens, how + often it happens + - other relevant information - log files, screenshots, etc. + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for +LLMUnity, including completely new features and minor improvements to +existing functionality. Following these guidelines helps maintainers and the +community understand your suggestion :pencil: and find related suggestions +:mag_right:. + +Before creating enhancement suggestions, please check [this section](#before-submitting-an-enhancement-suggestion) +as you might find out that you don't need to create one. When you are creating +an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). + +#### Before Submitting An Enhancement Suggestion + +**Perform a [cursory search](https://github.com/undreamai/LLMUnity/labels/enhancement)** +to see if the enhancement has already been suggested. If it has, add a +:thumbsup: to indicate your interest in it, or comment if there is additional +information you would like to add. + +#### How Do I Submit A (Good) Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). + +Simply create an issue on the [LLMUnity issue tracker](https://github.com/undreamai/LLMUnity/issues), choose the appropriate provided issue template and fill it out and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the + suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as + much detail as possible. This additional context helps the maintainers to + understand the enhancement from your perspective +* **Explain why this enhancement would be useful** to LLMUnity users. +* **Include screenshots and animated GIFs** if relevant to help you demonstrate + the steps or point out the part of LLMUnity which the suggestion is + related to. You can use [this tool](http://www.cockos.com/licecap/) to record + GIFs on macOS and Windows. +* **List some other applications where this enhancement exists, if applicable.** + +### Good First Issue + +We'll identify enhancements or bugs that can be categorized as tasks that: + + - have low impact, or have a known workaround + - should be fixed + - have a narrow scope and/or easy reproduction steps + - can be worked on independent of other tasks + +These issues will be labelled as [`good-first-issue`](https://github.com/undreamai/LLMUnity/labels/good%20first%20issue) +in the repository. If you are interested in contributing to the project, please +comment on the issue to let the maintainers (and community) know you are +interested in picking this up. + +### Issue and Pull Request Labels + +See [this page](https://github.com/undreamai/LLMUnity/labels) for the list of the labels we use to help us track and manage issues and pull requests. + + + + diff --git a/CONTRIBUTING.md.meta b/CONTRIBUTING.md.meta new file mode 100644 index 00000000..a5b77518 --- /dev/null +++ b/CONTRIBUTING.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0cf827c3ba62e4d598d999b5d39345b5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 7326118568ad2816e6045038bdde4d0d280f6ffe Mon Sep 17 00:00:00 2001 From: Lorenzo Toniazzi Date: Thu, 1 Aug 2024 13:09:43 +0100 Subject: [PATCH 104/105] docs: refer to crontributing md in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 865e2f95..3fcdc83f 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger ## How to help - [⭐ Star](https://github.com/undreamai/LLMUnity) the repo, leave us a [review](https://assetstore.unity.com/packages/slug/273604) and spread the word about the project! - Join us at [Discord](https://discord.gg/RwXKQb6zdv) and say hi! -- Submit feature requests or bugs as issues or even submit a PR and become a collaborator +- [Contribute](CONTRIBUTING.md) by submitting feature requests or bugs as issues or even submiting a PR and become a collaborator! ## Games using LLM for Unity - [Verbal Verdict](https://store.steampowered.com/app/2778780/Verbal_Verdict/) From 105c3235ae649d3349f3c8ec42188a92f1eee3fe Mon Sep 17 00:00:00 2001 From: Antonis Makropoulos Date: Thu, 8 Aug 2024 17:14:41 +0300 Subject: [PATCH 105/105] update changelogs --- CHANGELOG.md | 2 +- CHANGELOG.release.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8da18137..f10a04b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - LLM model manager (PR: #196) - Add Llama 3 7B and Qwen2 0.5B models (PR: #198) - Start LLM always asynchronously (PR: #199) - +- Add contributing guidelines (PR: #201) ## v2.0.3 #### 🚀 Features diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index 6bf38b7d..34a829c4 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -5,4 +5,4 @@ - LLM model manager (PR: #196) - Add Llama 3 7B and Qwen2 0.5B models (PR: #198) - Start LLM always asynchronously (PR: #199) - +- Add contributing guidelines (PR: #201)