using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace OnDeviceAgent.AgentCore { public sealed class SearchKnowledgeTool : AgentToolBase { readonly Func> m_Search; readonly Func> m_TranslateToEnglish; readonly Action m_OnNote; readonly string m_Subject; public SearchKnowledgeTool( Func> search, Func> translateToEnglish = null, Action onNote = null, string subject = null) { m_Search = search; m_TranslateToEnglish = translateToEnglish; m_OnNote = onNote; m_Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim(); } public override string Name => "SearchKnowledge"; public override string Description => "Search the indexed knowledge base for facts about " + (m_Subject ?? "the documented subject (its features, settings, specs, and procedures)") + ". Use this FIRST for ANY question about it; answer only from the returned passages, never from your own knowledge. If nothing relevant is returned, you may fall back to WebSearch."; public override AITool ToAIFunction(AgentToolContext context) { Task Run( [System.ComponentModel.Description("what to look up in the indexed knowledge base — phrase as a concrete question or topic (e.g. exact component name, setting, or behavior).")] string query = "") => InvokeAsync(context, query); return AIFunctionFactory.Create( (Func>)Run, name: Name, description: Description); } Task InvokeAsync(AgentToolContext ctx, string query) { var argsJson = "{\"query\":\"" + (query ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"}"; Func> body = m_Search == null ? null : async () => { var searchQuery = await ResolveSearchQueryAsync(ctx, query).ConfigureAwait(false); var result = await m_Search(searchQuery, ctx.CancellationToken).ConfigureAwait(false); return string.IsNullOrWhiteSpace(result) ? "No relevant passage found in the knowledge base. If the public web could answer this, call WebSearch with concise keywords; otherwise tell the user you don't have that information." : "Knowledge passages:\n" + result; }; return RunGuarded(ctx, argsJson, body, "The knowledge search is not available."); } // Translate a non-English query to English for retrieval; falls back to the original query. async Task ResolveSearchQueryAsync(AgentToolContext ctx, string query) { if (string.IsNullOrWhiteSpace(query) || !IsLikelyNonEnglish(query)) return query; if (m_TranslateToEnglish != null) { try { var translated = (await m_TranslateToEnglish(query, ctx.CancellationToken).ConfigureAwait(false))?.Trim(); if (!string.IsNullOrWhiteSpace(translated)) { await EmitNoteAsync(ctx, "🌐 " + query.Trim() + " → " + translated).ConfigureAwait(false); return translated; } } catch (OperationCanceledException) { throw; } catch (Exception e) { ctx.Log?.Invoke("[SearchKnowledge] translation failed: " + e.Message); } } // No translator (or it failed): the multilingual embedder still matches cross-lingually. await EmitNoteAsync(ctx, "🌐 " + query.Trim() + " (cross-lingual search)").ConfigureAwait(false); return query; } // UI events must fire on the Unity main thread; tool bodies run off the main thread. Task EmitNoteAsync(AgentToolContext ctx, string note) { if (m_OnNote == null || ctx.Dispatcher == null) return Task.CompletedTask; return ctx.Dispatcher.RunOnMainAsync(() => { m_OnNote(note); return Task.CompletedTask; }); } // True when the text has a letter outside Latin scripts (Hangul, CJK, Cyrillic, kana). Accented Latin stays English. static bool IsLikelyNonEnglish(string text) { for (var i = 0; i < text.Length; i++) if (text[i] > 'ɏ' && char.IsLetter(text[i])) return true; return false; } } }