com.sky.ondeviceagent / Runtime /AgentCore /Tools /Builtin /SearchKnowledgeTool.cs
Sky-Kim's picture
Initial commit
2e7837a
Raw
History Blame Contribute Delete
4.85 kB
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace OnDeviceAgent.AgentCore
{
public sealed class SearchKnowledgeTool : AgentToolBase
{
readonly Func<string, CancellationToken, Task<string>> m_Search;
readonly Func<string, CancellationToken, Task<string>> m_TranslateToEnglish;
readonly Action<string> m_OnNote;
readonly string m_Subject;
public SearchKnowledgeTool(
Func<string, CancellationToken, Task<string>> search,
Func<string, CancellationToken, Task<string>> translateToEnglish = null,
Action<string> 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<string> 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<string, Task<string>>)Run, name: Name, description: Description);
}
Task<string> InvokeAsync(AgentToolContext ctx, string query)
{
var argsJson = "{\"query\":\"" + (query ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"}";
Func<Task<string>> 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<string> 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;
}
}
}