| 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."); |
| } |
|
|
| |
| 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); } |
| } |
|
|
| |
| await EmitNoteAsync(ctx, "π " + query.Trim() + " (cross-lingual search)").ConfigureAwait(false); |
| return query; |
| } |
|
|
| |
| 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; }); |
| } |
|
|
| |
| static bool IsLikelyNonEnglish(string text) |
| { |
| for (var i = 0; i < text.Length; i++) |
| if (text[i] > 'Ι' && char.IsLetter(text[i])) |
| return true; |
| return false; |
| } |
| } |
| } |
|
|