com.sky.ondeviceagent / Runtime /AgentCore /Rag /KnowledgeRagComponent.cs
Sky-Kim's picture
Gate LiteRT-LM backend by SoC (NPU on verified chips, GPU elsewhere)
fc540fe
Raw
History Blame Contribute Delete
5.24 kB
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using OnDeviceAgent.Inference;
using OnDeviceAgent.RagLlm;
namespace OnDeviceAgent.AgentCore
{
[DisallowMultipleComponent]
public sealed class KnowledgeRagComponent : MonoBehaviour
{
[Header("Knowledge RAG: LightRAG knowledge graph (multilingual-e5-small embeddings)")]
[SerializeField] string m_TextModelPath = "Model/E5/e5_small_fp16.sentis";
[SerializeField] string m_TokenizerFile = "Model/E5/tokenizer.json";
[SerializeField, Tooltip("StreamingAssets-relative LightRAG DB folder built by OnDeviceAgent ▸ RAG ▸ Ingest Knowledge to DB.")]
string m_DbDir = "VoiceAgent/DB";
[SerializeField, Min(1)] int m_RagTopK = 6;
[SerializeField, Range(0f, 1f), Tooltip("Min e5 query↔chunk cosine for a query to count as on-topic for the indexed knowledge base. " +
"Below this, LightRAG is skipped so off-topic questions don't bloat the small-model prompt. 0 = always run RAG. " +
"Calibrated 2026-06: on-topic top-cosine ≥0.82, off-topic ≤0.79 → 0.78 cleanly separates them.")]
float m_RagRelevanceThreshold = 0.78f;
[Header("Retrieval mode (LightRAG)")]
[SerializeField, Tooltip("LightRAG retrieval mode. Naive = e5 vector RAG (best for specific factual lookups; " +
"graph context is noise there). Mix = graph knowledge-graph RAG (best for high-level / cross-section " +
"synthesis). Local/Global/Hybrid = LightRAG graph variants.")]
RagQueryMode m_QueryMode = RagQueryMode.Naive;
[SerializeField, Min(1), Tooltip("Chunk budget for graph (Mix-family) modes; synthesis gathers across sections. Naive uses Top-K.")]
int m_MixTopK = 12;
[Header("Ingest (Editor only, used by OnDeviceAgent ▸ RAG ▸ Ingest Knowledge to DB)")]
[SerializeField, Tooltip("Ollama endpoint used during knowledge ingest. Cloud-routed models (*-cloud) reach through the same endpoint.")]
string m_IngestEndpoint = "http://localhost:11434";
[SerializeField, OllamaModelDropdown(endpointField: "m_IngestEndpoint"),
Tooltip("LLM used for entity / relation extraction during ingest. A larger model improves KG quality and can differ from the runtime model.")]
string m_IngestModel = "gemma4:e2b";
public string IngestEndpoint => m_IngestEndpoint;
public string IngestModel => m_IngestModel;
LightRagKnowledgeService m_Rag;
// Disposed via m_Rag, but exposed so the agent reuses this instance to gate skills instead of loading a second model copy.
ITextEmbedder m_Embedder;
public ITextEmbedder TextEmbedder => m_Embedder;
public bool IsEnabled => enabled;
public bool Initialize(string endpoint, string model, IUnityMainThreadDispatcher dispatcher)
{
if (!enabled) return false;
if (dispatcher == null) { Debug.LogError("[RAG] dispatcher missing."); return false; }
if (m_Rag != null) return true;
var dbWorkingDir = StreamingDbInstaller.Install(m_DbDir, Debug.Log);
var embedder = new E5TextEmbedder(log: Debug.Log);
if (!embedder.Load(m_TextModelPath, m_TokenizerFile))
{
embedder.Dispose();
Debug.LogWarning("[RAG] e5 text embedder load failed, knowledge RAG disabled.");
return false;
}
m_Embedder = embedder;
#if UNITY_ANDROID && !UNITY_EDITOR
// Route RAG's LLM through the shared LiteRT-LM singleton - Ollama HTTP doesn't exist on-device and reusing avoids a second model load.
// Match the main transport's SoC-gated backend so this doesn't request a conflicting one for the shared engine.
var androidTransport = new AndroidLlmTransport(dispatcher, LiteRtModelProvisioner.UseNpu ? "NPU" : "GPU", Debug.Log);
IChatLlm ragLlm = new AndroidChatLlm(
(sys, prompt, ct) => androidTransport.RunAsync(sys, prompt, null, null, ct), model);
m_Rag = new LightRagKnowledgeService(dbWorkingDir, ragLlm, embedder, dispatcher,
config: null, log: Debug.Log,
queryMode: m_QueryMode, relevanceThreshold: m_RagRelevanceThreshold, mixTopK: m_MixTopK);
#else
m_Rag = new LightRagKnowledgeService(dbWorkingDir, endpoint, model, 8192, embedder, dispatcher, Debug.Log,
queryMode: m_QueryMode, relevanceThreshold: m_RagRelevanceThreshold, mixTopK: m_MixTopK);
#endif
_ = m_Rag.InitializeAsync();
return true;
}
public void Wire(BuiltinToolDependencies deps)
{
if (m_Rag == null || deps == null) return;
deps.SearchKnowledge = SearchKnowledgeTextAsync;
}
async Task<string> SearchKnowledgeTextAsync(string query, CancellationToken ct)
{
if (m_Rag == null) return null;
return await m_Rag.SearchAsync(query, m_RagTopK);
}
void OnDestroy()
{
m_Rag?.Dispose();
m_Rag = null;
m_Embedder = null;
}
}
}