diff --git a/README.md b/README.md index add4b72f..d0c26f2a 100644 --- a/README.md +++ b/README.md @@ -151,19 +151,18 @@ It is also a good idea to enable the `Download on Build` option in the LLM GameO
Save / Load your chat history -To automatically save / load your chat history, you can specify the `Save` parameter of the LLMCharacter to the filename (or relative path) of your choice. -The file is saved in the [persistentDataPath folder of Unity](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html). -This also saves the state of the LLM which means that the previously cached prompt does not need to be recomputed. +Your `LLMCharacter` components will automatically create corresponding `LLMChatHistory` components to store their chat histories. +- If you don't want to save the chat history, set the `EnableAutoSave` of the `LLMChatHistory` to false. +- You can specify the filename to use by setting the `ChatHistoryFilename` of the `LLMChatHistory`. The file is saved in the [persistentDataPath folder of Unity](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html). To manually save your chat history, you can use: ``` c# - llmCharacter.Save("filename"); + llmChatHistory.Save(); ``` and to load the history: ``` c# - llmCharacter.Load("filename"); + llmChatHistory.Load(); ``` -where filename the filename or relative path of your choice.
@@ -452,8 +451,8 @@ If the user's GPU is not supported, the LLM will fall back to the CPU - `Port` port of the LLM server (if `Remote` is set) - `Num Retries` number of HTTP request retries from the LLM server (if `Remote` is set) - `API key` API key of the LLM server (if `Remote` is set) --
Save save filename or relative path If set, the chat history and LLM state (if save cache is enabled) is automatically saved to file specified.
The chat history is saved with a json suffix and the LLM state with a cache suffix.
Both files are saved in the [persistentDataPath folder of Unity](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html).
-- `Save Cache` select to save the LLM state along with the chat history. The LLM state is typically around 100MB+. +-
Cache Filename save filename or relative path If set, the LLM state (if save cache is enabled) is automatically saved to file specified.
The LLM state is saved with a cache suffix.
The file is saved in the [persistentDataPath folder of Unity](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html).
+- `Save Cache` select to save the LLM state. The LLM state is typically around 100MB+. - `Debug Prompt` select to log the constructed prompts in the Unity Editor #### 🗨️ Chat Settings diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 8b7ee452..960f6318 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -32,9 +32,7 @@ public class LLMCharacter : MonoBehaviour [Remote] public int numRetries = 10; /// allows to use a server with API key [Remote] public string APIKey; - /// file to save the chat history. - /// The file is saved only for Chat calls with addToHistory set to true. - /// The file will be saved within the persistentDataPath directory (see https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html). + /// filename to use when saving the cache or chat history. [LLM] public string save = ""; /// toggle to save the LLM cache. This speeds up the prompt calculation but also requires ~100MB of space per character. [LLM] public bool saveCache = false; @@ -112,6 +110,14 @@ public class LLMCharacter : MonoBehaviour /// By providing a token ID and a positive or negative bias value, you can increase or decrease the probability of that token being generated. public Dictionary logitBias = null; + /// the chat history component that this character uses to store it's chat messages + public LLMChatHistory chatHistory { + get { return _chatHistory; } + set { + _chatHistory = value; + isCacheInvalid = true; + } + } /// the name of the player [Chat] public string playerName = "user"; /// the name of the AI @@ -122,13 +128,14 @@ public class LLMCharacter : MonoBehaviour public bool setNKeepToPrompt = true; /// \cond HIDE - public List chat; - private SemaphoreSlim chatLock = new SemaphoreSlim(1, 1); + [SerializeField, Chat] + private LLMChatHistory _chatHistory; private string chatTemplate; private ChatTemplate template = null; public string grammarString; private List<(string, string)> requestHeaders; private List WIPRequests = new List(); + private bool isCacheInvalid = false; /// \endcond /// @@ -140,7 +147,7 @@ public class LLMCharacter : MonoBehaviour /// - the chat template is constructed /// - the number of tokens to keep are based on the system prompt (if setNKeepToPrompt=true) /// - public void Awake() + public async void Awake() { // Start the LLM server in a cross-platform way if (!enabled) return; @@ -163,7 +170,8 @@ public void Awake() } InitGrammar(); - InitHistory(); + await InitHistory(); + await LoadCache(); } void OnValidate() @@ -215,60 +223,19 @@ void SortBySceneAndHierarchy(LLM[] array) } } - protected void InitHistory() + protected async Task InitHistory() { - InitPrompt(); - _ = LoadHistory(); - } - - protected async Task LoadHistory() - { - if (save == "" || !File.Exists(GetJsonSavePath(save))) return; - await chatLock.WaitAsync(); // Acquire the lock - try - { - await Load(save); - } - finally - { - chatLock.Release(); // Release the lock + // If no specific chat history object has been assigned to this character, create one. + if (chatHistory == null) { + chatHistory = gameObject.AddComponent(); + chatHistory.ChatHistoryFilename = save; + await chatHistory.Load(); } } - public virtual string GetSavePath(string filename) + public virtual string GetCacheSavePath() { - return Path.Combine(Application.persistentDataPath, filename).Replace('\\', '/'); - } - - public virtual string GetJsonSavePath(string filename) - { - return GetSavePath(filename + ".json"); - } - - public virtual string GetCacheSavePath(string filename) - { - return GetSavePath(filename + ".cache"); - } - - private void InitPrompt(bool clearChat = true) - { - if (chat != null) - { - if (clearChat) chat.Clear(); - } - else - { - chat = new List(); - } - ChatMessage promptMessage = new ChatMessage { role = "system", content = prompt }; - if (chat.Count == 0) - { - chat.Add(promptMessage); - } - else - { - chat[0] = promptMessage; - } + return Path.Combine(Application.persistentDataPath, save + ".cache").Replace('\\', '/'); } /// @@ -276,11 +243,15 @@ private void InitPrompt(bool clearChat = true) /// /// the system prompt /// whether to clear (true) or keep (false) the current chat history on top of the system prompt. - public void SetPrompt(string newPrompt, bool clearChat = true) + public async Task SetPrompt(string newPrompt, bool clearChat = true) { prompt = newPrompt; nKeep = -1; - InitPrompt(clearChat); + + if (clearChat) { + // Clear any existing messages + await chatHistory?.Clear(); + } } private bool CheckTemplate() @@ -293,12 +264,16 @@ private bool CheckTemplate() return true; } + private ChatMessage GetSystemPromptMessage() { + return new ChatMessage() { role = LLMConstants.SYSTEM_ROLE, content = prompt }; + } + private async Task InitNKeep() { if (setNKeepToPrompt && nKeep == -1) { if (!CheckTemplate()) return false; - string systemPrompt = template.ComputePrompt(new List(){chat[0]}, playerName, "", false); + string systemPrompt = template.ComputePrompt(new List(){GetSystemPromptMessage()}, playerName, "", false); List tokens = await Tokenize(systemPrompt); if (tokens == null) return false; SetNKeep(tokens); @@ -400,20 +375,19 @@ ChatRequest GenerateRequest(string prompt) return chatRequest; } - public void AddMessage(string role, string content) + public async Task AddMessage(string role, string content) { - // add the question / answer to the chat list, update prompt - chat.Add(new ChatMessage { role = role, content = content }); + await chatHistory.AddMessage(role, content); } - public void AddPlayerMessage(string content) + public async Task AddPlayerMessage(string content) { - AddMessage(playerName, content); + await AddMessage(playerName, content); } - public void AddAIMessage(string content) + public async Task AddAIMessage(string content) { - AddMessage(AIName, content); + await AddMessage(AIName, content); } protected string ChatContent(ChatResult result) @@ -490,44 +464,39 @@ protected string SlotContent(SlotResult result) /// the LLM response public async Task Chat(string query, Callback callback = null, EmptyCallback completionCallback = null, bool addToHistory = true) { - // handle a chat message by the user - // call the callback function while the answer is received - // call the completionCallback function when the answer is fully received await LoadTemplate(); if (!CheckTemplate()) return null; if (!await InitNKeep()) return null; + + var playerMessage = new ChatMessage() { role = playerName, content = query }; - string json; - await chatLock.WaitAsync(); - try - { - AddPlayerMessage(query); - string prompt = template.ComputePrompt(chat, playerName, AIName); - json = JsonUtility.ToJson(GenerateRequest(prompt)); - chat.RemoveAt(chat.Count - 1); - } - finally - { - chatLock.Release(); - } + // Setup the full list of messages for the current request + List promptMessages = chatHistory ? + await chatHistory.GetChatMessages() : + new List(); + promptMessages.Insert(0, GetSystemPromptMessage()); + promptMessages.Add(playerMessage); - string result = await CompletionRequest(json, callback); + // Prepare the request + string formattedPrompt = template.ComputePrompt(promptMessages, playerName, AIName); + string requestJson = JsonUtility.ToJson(GenerateRequest(formattedPrompt)); + + // Call the LLM + string result = await CompletionRequest(requestJson, callback); + // Update our chat history if required if (addToHistory && result != null) { - await chatLock.WaitAsync(); - try - { - AddPlayerMessage(query); - AddAIMessage(result); - } - finally - { - chatLock.Release(); - } - if (save != "") _ = Save(save); + await _chatHistory.AddMessages( + new List { + new ChatMessage { role = playerName, content = query }, + new ChatMessage { role = AIName, content = result } + } + ); } + await SaveCache(); + completionCallback?.Invoke(); return result; } @@ -634,46 +603,29 @@ protected async Task Slot(string filepath, string action) } /// - /// Saves the chat history and cache to the provided filename / relative path. + /// Saves the cache to the provided filename / relative path. /// - /// filename / relative path to save the chat history + /// filename / relative path to save the cache /// - public virtual async Task Save(string filename) + public virtual async Task SaveCache() { - string filepath = GetJsonSavePath(filename); - string dirname = Path.GetDirectoryName(filepath); - if (!Directory.Exists(dirname)) Directory.CreateDirectory(dirname); - string json = JsonUtility.ToJson(new ChatListWrapper { chat = chat.GetRange(1, chat.Count - 1) }); - File.WriteAllText(filepath, json); - - string cachepath = GetCacheSavePath(filename); if (remote || !saveCache) return null; - string result = await Slot(cachepath, "save"); + string result = await Slot(GetCacheSavePath(), "save"); + + // We now have a valid cache + isCacheInvalid = false; + return result; } /// - /// Load the chat history and cache from the provided filename / relative path. + /// Load the prompt cache. /// - /// filename / relative path to load the chat history from - /// - public virtual async Task Load(string filename) + public virtual async Task LoadCache() { - string filepath = GetJsonSavePath(filename); - if (!File.Exists(filepath)) - { - LLMUnitySetup.LogError($"File {filepath} does not exist."); - return null; - } - string json = File.ReadAllText(filepath); - List chatHistory = JsonUtility.FromJson(json).chat; - InitPrompt(true); - chat.AddRange(chatHistory); - LLMUnitySetup.Log($"Loaded {filepath}"); - - string cachepath = GetCacheSavePath(filename); - if (remote || !saveCache || !File.Exists(GetSavePath(cachepath))) return null; - string result = await Slot(cachepath, "restore"); + if (remote || !saveCache || isCacheInvalid || !File.Exists(GetCacheSavePath())) return null; + + string result = await Slot(GetCacheSavePath(), "restore"); return result; } @@ -843,6 +795,39 @@ protected async Task PostRequest(string json, string endpoint, Co if (remote) return await PostRequestRemote(json, endpoint, getContent, callback); return await PostRequestLocal(json, endpoint, getContent, callback); } + + #region Obsolete Functions + + [Obsolete] + public virtual async Task Save(string filename) { + + if (chatHistory) { + await chatHistory.Save(); + } + + return await SaveCache(); + } + + [Obsolete] + public virtual async Task Load(string filename) { + + if (chatHistory) { + chatHistory.ChatHistoryFilename = filename; + await chatHistory.Load(); + } + + save = filename; + return await LoadCache(); + } + + [Obsolete] + public virtual string GetSavePath(string filename) + { + return _chatHistory.GetChatHistoryFilePath(); + } + + #endregion + } /// \cond HIDE diff --git a/Runtime/LLMChatHistory.cs b/Runtime/LLMChatHistory.cs new file mode 100644 index 00000000..df49d5d8 --- /dev/null +++ b/Runtime/LLMChatHistory.cs @@ -0,0 +1,156 @@ +/// @file +/// @brief File implementing the LLMChatHistory. +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace LLMUnity +{ + /// @ingroup llm + /// + /// Manages a single instance of a chat history. + /// + public class LLMChatHistory : MonoBehaviour + { + /// + /// The name of the file where this chat history will be saved. + /// The file will be saved within the persistentDataPath directory (see https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html). + /// + public string ChatHistoryFilename = string.Empty; + + /// + /// If true, this component will automatically save a copy of its data to the filesystem with each update. + /// + public bool EnableAutoSave = true; + + /// + /// The current chat history + /// + protected List _chatHistory; + + /// + /// Ensures we're not trying to update the chat while saving or loading + /// + protected SemaphoreSlim _chatLock = new SemaphoreSlim(1, 1); + + /// + /// The Unity Awake function that initializes the state before the application starts. + /// + public async void Awake() + { + // If a filename has been provided for the chat history, attempt to load it + if (ChatHistoryFilename != string.Empty) { + await Load(); + } + else { + _chatHistory = new List(); + } + } + + public async Task AddMessages(List messages) + { + await WithChatLock(async () => { + await Task.Run(() => _chatHistory.AddRange(messages)); + }); + + if (EnableAutoSave) { + _ = Save(); + } + } + + public async Task AddMessage(string role, string content) + { + await AddMessages(new List { new ChatMessage { role = role, content = content } }); + } + + public async Task> GetChatMessages() { + + List chatMessages = null; + + await WithChatLock(async () => { + await Task.Run(() => chatMessages = new List(_chatHistory)); + }); + + return chatMessages; + } + + /// + /// Saves the chat history to the file system + /// + public async Task Save() + { + // If no filename has been provided, create one + if (ChatHistoryFilename == string.Empty) { + ChatHistoryFilename = $"chat_{Guid.NewGuid()}"; + } + + string filePath = GetChatHistoryFilePath(); + string directoryName = Path.GetDirectoryName(filePath); + + // Ensure the directory exists + if (!Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); + + // Save the chat history as json + await WithChatLock(async () => { + string json = JsonUtility.ToJson(new ChatListWrapper { chat = _chatHistory }); + await File.WriteAllTextAsync(filePath, json); + }); + } + + /// + /// Load the chat history from the file system + /// + public async Task Load() + { + string filePath = GetChatHistoryFilePath(); + + if (!File.Exists(filePath)) + { + LLMUnitySetup.LogError($"File {filePath} does not exist."); + return; + } + + // Load the chat from the json file + await WithChatLock(async () => { + string json = await File.ReadAllTextAsync(filePath); + _chatHistory = JsonUtility.FromJson(json).chat; + LLMUnitySetup.Log($"Loaded {filePath}"); + }); + } + + /// + /// Clears out the current chat history. + /// + public async Task Clear() { + await WithChatLock(async () => { + await Task.Run(() => _chatHistory.Clear()); + }); + + if (EnableAutoSave) { + _ = Save(); + } + } + + public bool IsEmpty() { + return _chatHistory?.Count == 0; + } + + public string GetChatHistoryFilePath() + { + return Path.Combine(Application.persistentDataPath, ChatHistoryFilename + ".json").Replace('\\', '/'); + } + + protected async Task WithChatLock(Func action) { + await _chatLock.WaitAsync(); + try { + await action(); + } + finally { + _chatLock.Release(); + } + } + } +} \ No newline at end of file diff --git a/Runtime/LLMChatHistory.cs.meta b/Runtime/LLMChatHistory.cs.meta new file mode 100644 index 00000000..3ca7ffbd --- /dev/null +++ b/Runtime/LLMChatHistory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b9aef079d11e8894bae3ae510742c32f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMChatTemplates.cs b/Runtime/LLMChatTemplates.cs index 0820078d..ec092c8c 100644 --- a/Runtime/LLMChatTemplates.cs +++ b/Runtime/LLMChatTemplates.cs @@ -174,7 +174,7 @@ public virtual string ComputePrompt(List messages, string playerNam { string chatPrompt = PromptPrefix(); int start = 0; - if (messages[0].role == "system") + if (messages[0].role == LLMConstants.SYSTEM_ROLE) { chatPrompt += RequestPrefix() + SystemPrefix() + messages[0].content + SystemSuffix(); start = 1; @@ -356,7 +356,7 @@ public class GemmaTemplate : ChatTemplate public override string ComputePrompt(List messages, string playerName, string AIName, bool endWithPrefix = true) { List messagesSystemPrompt = messages; - if (messages[0].role == "system") + if (messages[0].role == LLMConstants.SYSTEM_ROLE) { string firstUserMessage = messages[0].content; int start = 1; @@ -466,7 +466,7 @@ public class Phi3Template : ChatTemplate public override string ComputePrompt(List messages, string playerName, string AIName, bool endWithPrefix = true) { List messagesSystemPrompt = messages; - if (messages[0].role == "system") + if (messages[0].role == LLMConstants.SYSTEM_ROLE) { string firstUserMessage = messages[0].content; int start = 1; diff --git a/Runtime/LLMConstants.cs b/Runtime/LLMConstants.cs new file mode 100644 index 00000000..082a696f --- /dev/null +++ b/Runtime/LLMConstants.cs @@ -0,0 +1,6 @@ +namespace LLMUnity { + static class LLMConstants { + + public const string SYSTEM_ROLE = "system"; + } +} \ No newline at end of file diff --git a/Runtime/LLMConstants.cs.meta b/Runtime/LLMConstants.cs.meta new file mode 100644 index 00000000..a2312884 --- /dev/null +++ b/Runtime/LLMConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54899fb2b22da0d4480f36b31baa3536 +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 index d7a58ae7..0d4e29d0 100644 --- a/Samples~/AndroidDemo/Scene.unity +++ b/Samples~/AndroidDemo/Scene.unity @@ -38,7 +38,6 @@ RenderSettings: 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: @@ -693,12 +692,15 @@ MonoBehaviour: llm: {fileID: 1047848254} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -720,12 +722,12 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: user - AIName: assistant - prompt: A chat between a curious human and an artificial intelligence assistant. + aiName: assistant + systemPrompt: 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: @@ -896,7 +898,7 @@ MonoBehaviour: m_FloatArgument: 0 m_StringArgument: m_BoolArgument: 0 - m_CallState: 0 + m_CallState: 2 --- !u!114 &724531322 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1335,6 +1337,13 @@ MonoBehaviour: model: chatTemplate: chatml lora: + loraWeights: + flashAttention: 0 + APIKey: + SSLCert: + SSLCertPath: + SSLKey: + SSLKeyPath: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/ChatBot/Scene.unity b/Samples~/ChatBot/Scene.unity index bea05e22..10faae40 100644 --- a/Samples~/ChatBot/Scene.unity +++ b/Samples~/ChatBot/Scene.unity @@ -38,7 +38,6 @@ RenderSettings: 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: @@ -671,6 +670,13 @@ MonoBehaviour: model: chatTemplate: chatml lora: + loraWeights: + flashAttention: 0 + APIKey: + SSLCert: + SSLCertPath: + SSLKey: + SSLKeyPath: --- !u!1 &1051131186 GameObject: m_ObjectHideFlags: 0 @@ -1131,12 +1137,15 @@ MonoBehaviour: llm: {fileID: 817827756} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -1158,12 +1167,12 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: user - AIName: assistant - prompt: A chat between a curious human and an artificial intelligence assistant. + aiName: assistant + systemPrompt: 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 &2011827136 GameObject: diff --git a/Samples~/KnowledgeBaseGame/Scene.unity b/Samples~/KnowledgeBaseGame/Scene.unity index ff10abb9..67338df7 100644 --- a/Samples~/KnowledgeBaseGame/Scene.unity +++ b/Samples~/KnowledgeBaseGame/Scene.unity @@ -38,7 +38,6 @@ RenderSettings: 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: @@ -4284,12 +4283,15 @@ MonoBehaviour: llm: {fileID: 2142407556} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -4311,18 +4313,14 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: Detective - AIName: - prompt: 'You are a robot working at a house where a diamond was stolen and a detective - asks you questions about the robbery. - - Answer the question provided at the - section "Question" based on the possible answers at the section "Answers". - - Rephrase - the answer, do not copy it directly.' + aiName: + systemPrompt: "You are a robot working at a house where a diamond was stolen and + a detective\r asks you questions about the robbery.\r\n\r\nAnswer the question + provided at the\r section \"Question\" based on the possible answers at the section + \"Answers\".\r\n\r\nRephrase\r the answer, do not copy it directly.'" setNKeepToPrompt: 1 - chat: [] grammarString: --- !u!4 &1275496424 Transform: @@ -7548,6 +7546,13 @@ MonoBehaviour: model: chatTemplate: chatml lora: + loraWeights: + flashAttention: 0 + APIKey: + SSLCert: + SSLCertPath: + SSLKey: + SSLKeyPath: --- !u!4 &2142407557 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/MultipleCharacters/Scene.unity b/Samples~/MultipleCharacters/Scene.unity index 75117583..b9017152 100644 --- a/Samples~/MultipleCharacters/Scene.unity +++ b/Samples~/MultipleCharacters/Scene.unity @@ -38,7 +38,6 @@ RenderSettings: 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: @@ -595,12 +594,15 @@ MonoBehaviour: llm: {fileID: 1047848254} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -622,13 +624,13 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: Human - AIName: Adam - prompt: A chat between a curious human and an artificial intelligence assistant + aiName: Adam + systemPrompt: A chat between a curious human and an artificial intelligence assistant named Adam. The assistant gives helpful, detailed, and polite answers to the human's questions. setNKeepToPrompt: 1 - chat: [] grammarString: --- !u!1 &726528676 GameObject: @@ -1527,6 +1529,13 @@ MonoBehaviour: model: chatTemplate: chatml lora: + loraWeights: + flashAttention: 0 + APIKey: + SSLCert: + SSLCertPath: + SSLKey: + SSLKeyPath: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 @@ -1961,12 +1970,15 @@ MonoBehaviour: llm: {fileID: 1047848254} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -1988,13 +2000,13 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: Human - AIName: Eve - prompt: A chat between a curious human and an artificial intelligence assistant + aiName: Eve + systemPrompt: A chat between a curious human and an artificial intelligence assistant named Eve. The assistant gives helpful, detailed, and polite answers to the human's questions. setNKeepToPrompt: 1 - chat: [] grammarString: --- !u!1 &1609985808 GameObject: diff --git a/Samples~/SimpleInteraction/Scene.unity b/Samples~/SimpleInteraction/Scene.unity index b4e5e246..1440d9d9 100644 --- a/Samples~/SimpleInteraction/Scene.unity +++ b/Samples~/SimpleInteraction/Scene.unity @@ -38,7 +38,6 @@ RenderSettings: 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: @@ -483,12 +482,15 @@ MonoBehaviour: llm: {fileID: 1047848254} host: localhost port: 13333 - save: + numRetries: -1 + APIKey: + cacheFilename: saveCache: 0 debugPrompt: 0 stream: 1 grammar: cachePrompt: 1 + slot: -1 seed: 0 numPredict: 256 temperature: 0.2 @@ -510,12 +512,12 @@ MonoBehaviour: ignoreEos: 0 nKeep: -1 stop: [] + chatHistory: {fileID: 0} playerName: user - AIName: assistant - prompt: A chat between a curious human and an artificial intelligence assistant. + aiName: assistant + systemPrompt: 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 &724531319 GameObject: @@ -1050,6 +1052,13 @@ MonoBehaviour: model: chatTemplate: chatml lora: + loraWeights: + flashAttention: 0 + APIKey: + SSLCert: + SSLCertPath: + SSLKey: + SSLKeyPath: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index 747e8af6..76544e92 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -77,6 +77,7 @@ public class TestLLM protected GameObject gameObject; protected LLM llm; protected LLMCharacter llmCharacter; + protected LLMChatHistory llmChatHistory; protected Exception error = null; protected string prompt; protected string query; @@ -233,29 +234,28 @@ public virtual async Task Tests() { await llmCharacter.Tokenize("I", TestTokens); await llmCharacter.Warmup(); - TestInitParameters(tokens1, 1); - TestWarmup(); + TestInitParameters(tokens1, 0); await llmCharacter.Chat(query, (string reply) => TestChat(reply, reply1)); - TestPostChat(3); - llmCharacter.SetPrompt(llmCharacter.prompt); + TestPostChat(2); + await llmCharacter.SetPrompt(llmCharacter.prompt); llmCharacter.AIName = "False response"; await llmCharacter.Chat(query, (string reply) => TestChat(reply, reply2)); - TestPostChat(3); + TestPostChat(2); await llmCharacter.Chat("bye!"); - TestPostChat(5); + TestPostChat(4); prompt = "How are you?"; - llmCharacter.SetPrompt(prompt); + await llmCharacter.SetPrompt(prompt); await llmCharacter.Chat("hi"); - TestInitParameters(tokens2, 3); + TestInitParameters(tokens2, 2); List embeddings = await llmCharacter.Embeddings("hi how are you?"); TestEmbeddings(embeddings); } - public void TestInitParameters(int nkeep, int chats) + public void TestInitParameters(int nKeep, int expectedMessageCount) { - Assert.AreEqual(llmCharacter.nKeep, nkeep); + Assert.AreEqual(llmCharacter.nKeep, nKeep); Assert.That(ChatTemplate.GetTemplate(llm.chatTemplate).GetStop(llmCharacter.playerName, llmCharacter.AIName).Length > 0); - Assert.AreEqual(llmCharacter.chat.Count, chats); + Assert.AreEqual(llmCharacter.chatHistory?.GetChatMessages().Result.Count, expectedMessageCount); } public void TestTokens(List tokens) @@ -263,20 +263,15 @@ public void TestTokens(List tokens) Assert.AreEqual(tokens, new List {40}); } - public void TestWarmup() + public void TestChat(string generatedReply, string expectedReply) { - Assert.That(llmCharacter.chat.Count == 1); - } - - public void TestChat(string reply, string replyGT) - { - Debug.Log(reply.Trim()); - Assert.That(reply.Trim() == replyGT); + Debug.Log(generatedReply.Trim()); + Assert.That(generatedReply.Trim() == expectedReply); } public void TestPostChat(int num) { - Assert.That(llmCharacter.chat.Count == num); + Assert.AreEqual(num, llmCharacter.chatHistory.GetChatMessages().Result.Count); } public void TestEmbeddings(List embeddings) @@ -457,31 +452,14 @@ public override LLMCharacter CreateLLMCharacter() public override async Task Tests() { await base.Tests(); - TestSave(); + TestSaveCache(); } - public void TestSave() + public void TestSaveCache() { - string jsonPath = llmCharacter.GetJsonSavePath(saveName); - string cachePath = llmCharacter.GetCacheSavePath(saveName); - Assert.That(File.Exists(jsonPath)); + string cachePath = llmCharacter.GetCacheSavePath(); Assert.That(File.Exists(cachePath)); - string json = File.ReadAllText(jsonPath); - File.Delete(jsonPath); File.Delete(cachePath); - - List chatHistory = JsonUtility.FromJson(json).chat; - Assert.AreEqual(chatHistory.Count, 2); - Assert.AreEqual(chatHistory[0].role, llmCharacter.playerName); - Assert.AreEqual(chatHistory[0].content, "hi"); - Assert.AreEqual(chatHistory[1].role, llmCharacter.AIName); - - Assert.AreEqual(llmCharacter.chat.Count, chatHistory.Count + 1); - for (int i = 0; i < chatHistory.Count; i++) - { - Assert.AreEqual(chatHistory[i].role, llmCharacter.chat[i + 1].role); - Assert.AreEqual(chatHistory[i].content, llmCharacter.chat[i + 1].content); - } } } } diff --git a/Tests/Runtime/TestLLMChatHistory.cs b/Tests/Runtime/TestLLMChatHistory.cs new file mode 100644 index 00000000..42033762 --- /dev/null +++ b/Tests/Runtime/TestLLMChatHistory.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LLMUnity; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LLMUnityTests +{ + public class TestLLMChatHistory + { + private GameObject _gameObject; + private LLMChatHistory _chatHistory; + + + [SetUp] + public void Setup() + { + // Create a new GameObject + _gameObject = new GameObject("TestObject"); + + // Add the component X to the GameObject + _chatHistory = _gameObject.AddComponent(); + } + + [Test] + public async void TestSaveAndLoad() + { + // 1. ARRANGE + // Add a few messages to save + await _chatHistory.AddMessage("user", "hello"); + await _chatHistory.AddMessage("ai", "hi"); + + // Save them off and grab the generated filename (since we didn't supply one) + await _chatHistory.Save(); + string filename = _chatHistory.ChatHistoryFilename; + + // 2. ACT + // Destroy the current chat history + Object.Destroy(_gameObject); + + // Recreate the chat history and load from the same file + Setup(); + _chatHistory.ChatHistoryFilename = filename; + await _chatHistory.Load(); + + // 3. ASSERT + // Validate the messages were loaded + List loadedMessages = await _chatHistory.GetChatMessages(); + Assert.AreEqual(loadedMessages.Count, 2); + Assert.AreEqual(loadedMessages[0].role, "user"); + Assert.AreEqual(loadedMessages[0].content, "hello"); + Assert.AreEqual(loadedMessages[1].role, "ai"); + Assert.AreEqual(loadedMessages[1].content, "hi"); + } + + [TearDown] + public void Teardown() + { + // Cleanup the GameObject after the test + Object.Destroy(_gameObject); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestLLMChatHistory.cs.meta b/Tests/Runtime/TestLLMChatHistory.cs.meta new file mode 100644 index 00000000..59db3f27 --- /dev/null +++ b/Tests/Runtime/TestLLMChatHistory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed6dca1c56c54d04caf905b0b1caf269 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: