mirror of
https://github.com/shinyflvre/Mate-Engine.git
synced 2026-07-26 01:13:33 +00:00
**Changelog 1.4.0 – THE LOCAL AI Update** **Disclaimer:** The AI runs *entirely locally* and is designed to be environmentally and ethically responsible. - It does **not** harm nature or exploit any artists. - It has only **1.5 billion parameters**, making it a **very small model** by modern standards. - It consumes as much GPU energy as watching a **1440p 60fps YouTube video**. - It was **not trained on artist data**; it uses personal input and public Wikipedia datasets. - It has its own personality, and I ensured it adheres to **ethical standards**. --- ### New Features - Added **Qwen 2.5 1.5B LLM** to MateEngine! - Press **Middle Mouse Button** while hovering over the pet to open the chat window. - Runs fully locally on your GPU — no internet connection required. - Only uses GPU while processing prompts. - Note: This is a **lightweight model** and won’t handle complex tasks or coding. It’s just a small, fun feature! - Added **Resize Button** – Quickly adjust app size for 1080p, 1440p, and 4K monitors. - Added **VRM Information Panel** – Displays polygon count, VRM version, and bone status: - **Perfect** – Properly exported model. - **Failure** – Model has export issues; animations may break. - Added **Options Submenu**. - Added **Volume Sliders** – Independently control pet, effects, and menu volume. - Added **Graphics Settings**. - Added **Dance Trails** – Glowing effects on the pet’s hands during dance. Toggle on/off as desired. - Improved **Music Source Selector** – You can now whitelist or add custom audio sources for dancing. - Added **MateEngine Version Info** – View the current app version from the menu. - Menu can now be opened with **M** key or **Right-Click**. - Added full support for **VRM 1.x** and **VRM 0.x** models. - Introduced **VRM Validator Tool** in Unity Editor – Great for content creators. --- ### Improvements & Fixes - Major **Performance Optimizations**. - Fixed a **GC Error** that could appear after 6–12 hours of continuous use. - Improved **Hand Tracking** – Reduced stuttering in some animations. - Added **Discord Rich Presence** integration. - Added **Glowing Halo** for Steam users as exclusive DLC!
387 lines
16 KiB
C#
387 lines
16 KiB
C#
/// @file
|
|
/// @brief File implementing the basic functionality for LLM callers.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
|
|
namespace LLMUnity
|
|
{
|
|
[DefaultExecutionOrder(-2)]
|
|
/// @ingroup llm
|
|
/// <summary>
|
|
/// Class implementing calling of LLM functions (local and remote).
|
|
/// </summary>
|
|
public class LLMCaller : MonoBehaviour
|
|
{
|
|
/// <summary> show/hide advanced options in the GameObject </summary>
|
|
[Tooltip("show/hide advanced options in the GameObject")]
|
|
[HideInInspector] public bool advancedOptions = false;
|
|
/// <summary> use remote LLM server </summary>
|
|
[Tooltip("use remote LLM server")]
|
|
[LocalRemote] public bool remote = false;
|
|
/// <summary> LLM GameObject to use </summary>
|
|
[Tooltip("LLM GameObject to use")] // Tooltip: ignore
|
|
[Local, SerializeField] protected LLM _llm;
|
|
public LLM llm
|
|
{
|
|
get => _llm;//whatever
|
|
set => SetLLM(value);
|
|
}
|
|
/// <summary> API key for the remote server </summary>
|
|
[Tooltip("API key for the remote server")]
|
|
[Remote] public string APIKey;
|
|
/// <summary> host of the remote LLM server </summary>
|
|
[Tooltip("host of the remote LLM server")]
|
|
[Remote] public string host = "localhost";
|
|
/// <summary> port of the remote LLM server </summary>
|
|
[Tooltip("port of the remote LLM server")]
|
|
[Remote] public int port = 13333;
|
|
/// <summary> number of retries to use for the remote LLM server requests (-1 = infinite) </summary>
|
|
[Tooltip("number of retries to use for the remote LLM server requests (-1 = infinite)")]
|
|
[Remote] public int numRetries = 10;
|
|
|
|
protected LLM _prellm;
|
|
protected List<(string, string)> requestHeaders;
|
|
protected List<UnityWebRequest> WIPRequests = new List<UnityWebRequest>();
|
|
|
|
/// <summary>
|
|
/// The Unity Awake function that initializes the state before the application starts.
|
|
/// The following actions are executed:
|
|
/// - the corresponding LLM server is defined (if ran locally)
|
|
/// - the grammar is set based on the grammar file
|
|
/// - the prompt and chat history are initialised
|
|
/// - the chat template is constructed
|
|
/// - the number of tokens to keep are based on the system prompt (if setNKeepToPrompt=true)
|
|
/// </summary>
|
|
public virtual void Awake()
|
|
{
|
|
// Start the LLM server in a cross-platform way
|
|
if (!enabled) return;
|
|
|
|
requestHeaders = new List<(string, string)> { ("Content-Type", "application/json") };
|
|
if (!remote)
|
|
{
|
|
AssignLLM();
|
|
if (llm == null)
|
|
{
|
|
string error = $"No LLM assigned or detected for LLMCharacter {name}!";
|
|
LLMUnitySetup.LogError(error);
|
|
throw new Exception(error);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!String.IsNullOrEmpty(APIKey)) requestHeaders.Add(("Authorization", "Bearer " + APIKey));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the LLM object of the LLMCaller
|
|
/// </summary>
|
|
/// <param name="llmSet">LLM object</param>
|
|
protected virtual void SetLLM(LLM llmSet)
|
|
{
|
|
if (llmSet != null && !IsValidLLM(llmSet))
|
|
{
|
|
LLMUnitySetup.LogError(NotValidLLMError());
|
|
llmSet = null;
|
|
}
|
|
_llm = llmSet;
|
|
_prellm = _llm;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a LLM is valid for the LLMCaller
|
|
/// </summary>
|
|
/// <param name="llmSet">LLM object</param>
|
|
/// <returns>bool specifying whether the LLM is valid</returns>
|
|
public virtual bool IsValidLLM(LLM llmSet)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a LLM can be auto-assigned if the LLM of the LLMCaller is null
|
|
/// </summary>
|
|
/// <param name="llmSet"LLM object></param>
|
|
/// <returns>bool specifying whether the LLM can be auto-assigned</returns>
|
|
public virtual bool IsAutoAssignableLLM(LLM llmSet)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
protected virtual string NotValidLLMError()
|
|
{
|
|
return $"Can't set LLM {llm.name} to {name}";
|
|
}
|
|
|
|
protected virtual void OnValidate()
|
|
{
|
|
if (_llm != _prellm) SetLLM(_llm);
|
|
AssignLLM();
|
|
}
|
|
|
|
protected virtual void Reset()
|
|
{
|
|
AssignLLM();
|
|
}
|
|
|
|
protected virtual void AssignLLM()
|
|
{
|
|
if (remote || llm != null) return;
|
|
|
|
List<LLM> validLLMs = new List<LLM>();
|
|
#if UNITY_6000_0_OR_NEWER
|
|
foreach (LLM foundllm in FindObjectsByType(typeof(LLM), FindObjectsSortMode.None))
|
|
#else
|
|
foreach (LLM foundllm in FindObjectsOfType<LLM>())
|
|
#endif
|
|
{
|
|
if (IsValidLLM(foundllm) && IsAutoAssignableLLM(foundllm)) validLLMs.Add(foundllm);
|
|
}
|
|
if (validLLMs.Count == 0) return;
|
|
|
|
llm = SortLLMsByBestMatching(validLLMs.ToArray())[0];
|
|
string msg = $"Assigning LLM {llm.name} to {GetType()} {name}";
|
|
if (llm.gameObject.scene != gameObject.scene) msg += $" from scene {llm.gameObject.scene}";
|
|
LLMUnitySetup.Log(msg);
|
|
}
|
|
|
|
protected virtual LLM[] SortLLMsByBestMatching(LLM[] arrayIn)
|
|
{
|
|
LLM[] array = (LLM[])arrayIn.Clone();
|
|
for (int i = 0; i < array.Length - 1; i++)
|
|
{
|
|
bool swapped = false;
|
|
for (int j = 0; j < array.Length - i - 1; j++)
|
|
{
|
|
bool sameScene = array[j].gameObject.scene == array[j + 1].gameObject.scene;
|
|
bool swap = (
|
|
(!sameScene && array[j + 1].gameObject.scene == gameObject.scene) ||
|
|
(sameScene && array[j].transform.GetSiblingIndex() > array[j + 1].transform.GetSiblingIndex())
|
|
);
|
|
if (swap)
|
|
{
|
|
LLM temp = array[j];
|
|
array[j] = array[j + 1];
|
|
array[j + 1] = temp;
|
|
swapped = true;
|
|
}
|
|
}
|
|
if (!swapped) break;
|
|
}
|
|
return array;
|
|
}
|
|
|
|
protected virtual List<int> TokenizeContent(TokenizeResult result)
|
|
{
|
|
// get the tokens from a tokenize result received from the endpoint
|
|
return result.tokens;
|
|
}
|
|
|
|
protected virtual string DetokenizeContent(TokenizeRequest result)
|
|
{
|
|
// get content from a chat result received from the endpoint
|
|
return result.content;
|
|
}
|
|
|
|
protected virtual List<float> EmbeddingsContent(EmbeddingsResult result)
|
|
{
|
|
// get content from a chat result received from the endpoint
|
|
return result.embedding;
|
|
}
|
|
|
|
protected virtual Ret ConvertContent<Res, Ret>(string response, ContentCallback<Res, Ret> getContent = null)
|
|
{
|
|
// template function to convert the json received and get the content
|
|
if (response == null) return default;
|
|
response = response.Trim();
|
|
if (response.StartsWith("data: "))
|
|
{
|
|
string responseArray = "";
|
|
foreach (string responsePart in response.Replace("\n\n", "").Split("data: "))
|
|
{
|
|
if (responsePart == "") continue;
|
|
if (responseArray != "") responseArray += ",\n";
|
|
responseArray += responsePart;
|
|
}
|
|
response = $"{{\"data\": [{responseArray}]}}";
|
|
}
|
|
return getContent(JsonUtility.FromJson<Res>(response));
|
|
}
|
|
|
|
protected virtual void CancelRequestsLocal() {}
|
|
|
|
protected virtual void CancelRequestsRemote()
|
|
{
|
|
foreach (UnityWebRequest request in WIPRequests)
|
|
{
|
|
request.Abort();
|
|
}
|
|
WIPRequests.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel the ongoing requests e.g. Chat, Complete.
|
|
/// </summary>
|
|
// <summary>
|
|
public virtual void CancelRequests()
|
|
{
|
|
if (remote) CancelRequestsRemote();
|
|
else CancelRequestsLocal();
|
|
}
|
|
|
|
protected virtual async Task<Ret> PostRequestLocal<Res, Ret>(string json, string endpoint, ContentCallback<Res, Ret> getContent, Callback<Ret> callback = null)
|
|
{
|
|
// send a post request to the server and call the relevant callbacks to convert the received content and handle it
|
|
// this function has streaming functionality i.e. handles the answer while it is being received
|
|
while (!llm.failed && !llm.started) await Task.Yield();
|
|
string callResult = null;
|
|
switch (endpoint)
|
|
{
|
|
case "tokenize":
|
|
callResult = await llm.Tokenize(json);
|
|
break;
|
|
case "detokenize":
|
|
callResult = await llm.Detokenize(json);
|
|
break;
|
|
case "embeddings":
|
|
callResult = await llm.Embeddings(json);
|
|
break;
|
|
case "slots":
|
|
callResult = await llm.Slot(json);
|
|
break;
|
|
default:
|
|
LLMUnitySetup.LogError($"Unknown endpoint {endpoint}");
|
|
break;
|
|
}
|
|
|
|
Ret result = ConvertContent(callResult, getContent);
|
|
callback?.Invoke(result);
|
|
return result;
|
|
}
|
|
|
|
protected virtual async Task<Ret> PostRequestRemote<Res, Ret>(string json, string endpoint, ContentCallback<Res, Ret> getContent, Callback<Ret> callback = null)
|
|
{
|
|
// send a post request to the server and call the relevant callbacks to convert the received content and handle it
|
|
// this function has streaming functionality i.e. handles the answer while it is being received
|
|
if (endpoint == "slots")
|
|
{
|
|
LLMUnitySetup.LogError("Saving and loading is not currently supported in remote setting");
|
|
return default;
|
|
}
|
|
|
|
Ret result = default;
|
|
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
|
|
UnityWebRequest request = null;
|
|
string error = null;
|
|
int tryNr = numRetries;
|
|
|
|
while (tryNr != 0)
|
|
{
|
|
using (request = UnityWebRequest.Put($"{host}:{port}/{endpoint}", jsonToSend))
|
|
{
|
|
WIPRequests.Add(request);
|
|
|
|
request.method = "POST";
|
|
if (requestHeaders != null)
|
|
{
|
|
for (int i = 0; i < requestHeaders.Count; i++)
|
|
request.SetRequestHeader(requestHeaders[i].Item1, requestHeaders[i].Item2);
|
|
}
|
|
|
|
// Start the request asynchronously
|
|
UnityWebRequestAsyncOperation asyncOperation = request.SendWebRequest();
|
|
await Task.Yield(); // Wait for the next frame so that asyncOperation is properly registered (especially if not in main thread)
|
|
|
|
float lastProgress = 0f;
|
|
// Continue updating progress until the request is completed
|
|
while (!asyncOperation.isDone)
|
|
{
|
|
float currentProgress = request.downloadProgress;
|
|
// Check if progress has changed
|
|
if (currentProgress != lastProgress && callback != null)
|
|
{
|
|
callback?.Invoke(ConvertContent(request.downloadHandler.text, getContent));
|
|
lastProgress = currentProgress;
|
|
}
|
|
// Wait for the next frame
|
|
await Task.Yield();
|
|
}
|
|
WIPRequests.Remove(request);
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
{
|
|
result = ConvertContent(request.downloadHandler.text, getContent);
|
|
error = null;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
result = default;
|
|
error = request.error;
|
|
if (request.responseCode == (int)System.Net.HttpStatusCode.Unauthorized) break;
|
|
}
|
|
}
|
|
tryNr--;
|
|
if (tryNr > 0) await Task.Delay(200 * (numRetries - tryNr));
|
|
}
|
|
|
|
if (error != null) LLMUnitySetup.LogError(error);
|
|
callback?.Invoke(result);
|
|
return result;
|
|
}
|
|
|
|
protected virtual async Task<Ret> PostRequest<Res, Ret>(string json, string endpoint, ContentCallback<Res, Ret> getContent, Callback<Ret> callback = null)
|
|
{
|
|
if (remote) return await PostRequestRemote(json, endpoint, getContent, callback);
|
|
return await PostRequestLocal(json, endpoint, getContent, callback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tokenises the provided query.
|
|
/// </summary>
|
|
/// <param name="query">query to tokenise</param>
|
|
/// <param name="callback">callback function called with the result tokens</param>
|
|
/// <returns>list of the tokens</returns>
|
|
public virtual async Task<List<int>> Tokenize(string query, Callback<List<int>> callback = null)
|
|
{
|
|
// handle the tokenization of a message by the user
|
|
TokenizeRequest tokenizeRequest = new TokenizeRequest();
|
|
tokenizeRequest.content = query;
|
|
string json = JsonUtility.ToJson(tokenizeRequest);
|
|
return await PostRequest<TokenizeResult, List<int>>(json, "tokenize", TokenizeContent, callback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detokenises the provided tokens to a string.
|
|
/// </summary>
|
|
/// <param name="tokens">tokens to detokenise</param>
|
|
/// <param name="callback">callback function called with the result string</param>
|
|
/// <returns>the detokenised string</returns>
|
|
public virtual async Task<string> Detokenize(List<int> tokens, Callback<string> callback = null)
|
|
{
|
|
// handle the detokenization of a message by the user
|
|
TokenizeResult tokenizeRequest = new TokenizeResult();
|
|
tokenizeRequest.tokens = tokens;
|
|
string json = JsonUtility.ToJson(tokenizeRequest);
|
|
return await PostRequest<TokenizeRequest, string>(json, "detokenize", DetokenizeContent, callback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the embeddings of the provided input.
|
|
/// </summary>
|
|
/// <param name="tokens">input to compute the embeddings for</param>
|
|
/// <param name="callback">callback function called with the result string</param>
|
|
/// <returns>the computed embeddings</returns>
|
|
public virtual async Task<List<float>> Embeddings(string query, Callback<List<float>> callback = null)
|
|
{
|
|
// handle the tokenization of a message by the user
|
|
TokenizeRequest tokenizeRequest = new TokenizeRequest();
|
|
tokenizeRequest.content = query;
|
|
string json = JsonUtility.ToJson(tokenizeRequest);
|
|
return await PostRequest<EmbeddingsResult, List<float>>(json, "embeddings", EmbeddingsContent, callback);
|
|
}
|
|
}
|
|
}
|