feat: Serper Web Search als Agent-Tool

- ToolService: serper_web_search Tool-Definition + ExecuteSerperWebSearch()
  Google-Suche via Serper.dev API mit Titel, Link, Snippet
  Knowledge Graph + Answer Box + organische Ergebnisse
- AgentRecord: UseSerperSearch Property (Default: false)
- DB-Migration: use_serper_search Spalte
- AnthropicService + ChatService: useSerperSearch Parameter durchreichen
- ChatEndpoint + AgentEndpoint: useSerperSearch in API
- Frontend: Checkbox, Badge (Serper Search), laden/speichern
- Aktivierung: Agent-Checkbox + SerperApiKey in Server-Einstellungen
This commit is contained in:
Meleki committed 2026-03-13 09:41:56 +01:00
1 parent 558ded1ed5
commit 192f41e91e
11 files changed
+150 -12

No files matched your search

+125 -2
View File
@@ -33,6 +33,7 @@ public class ToolService
private int ExecuteTimeoutSeconds => _config.GetValue<int>("ExecuteTimeout", 180);
public bool HasGitHubToken => !string.IsNullOrWhiteSpace(_config["GitHubToken"]);
public bool HasSerperKey => !string.IsNullOrWhiteSpace(_config["SerperApiKey"]);
public const string AskUserChoiceToolName = "ask_user_choice";
/// <summary>
@@ -43,7 +44,7 @@ public class ToolService
{
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
"memory_read", "memory_write", "memory_list", "memory_search",
"remote_execute", "github_code_search", AskUserChoiceToolName
"remote_execute", "github_code_search", "serper_web_search", AskUserChoiceToolName
};
public object[] FilterReadOnlyTools(object[] tools)
@@ -63,7 +64,7 @@ public class ToolService
return filtered.ToArray();
}
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false, bool includeGitHubSearch = false)
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false, bool includeGitHubSearch = false, bool includeSerperSearch = false)
{
var allTools = new List<object>();
if (includeUserChoice)
@@ -80,6 +81,8 @@ public class ToolService
allTools.Add(GetSendToSessionTool());
if (includeGitHubSearch)
allTools.Add(GetGitHubCodeSearchTool());
if (includeSerperSearch)
allTools.Add(GetSerperWebSearchTool());
return allTools.ToArray();
}
@@ -814,6 +817,10 @@ public class ToolService
{
return await ExecuteGitHubCodeSearch(input, cancellationToken);
}
case "serper_web_search":
{
return await ExecuteSerperWebSearch(input, cancellationToken);
}
default:
return $"Fehler: Unbekanntes Tool: {name}";
}
@@ -929,4 +936,120 @@ public class ToolService
return $"GitHub-Suche fehlgeschlagen: {ex.Message}";
}
}
// ── Serper Web Search ──
private object GetSerperWebSearchTool()
{
return new
{
name = "serper_web_search",
description = "Durchsucht das Internet via Google (Serper.dev API). Gibt strukturierte Suchergebnisse zurück mit Titel, Link und Snippet. Unterstützt alle Google-Suchoperatoren (z.B. exakte Phrasen mit Anführungszeichen, site:, filetype:). Nutze dies für Webrecherche, aktuelle Informationen oder das Finden spezifischer Inhalte im Internet.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["query"] = new { type = "string", description = "Suchbegriff (unterstützt Google-Suchsyntax, z.B. '\"exakte Phrase\"', 'site:github.com', 'filetype:pdf')" },
["num"] = new { type = "integer", description = "Anzahl Ergebnisse (1-100, Default: 10)" }
},
required = new[] { "query" }
}
};
}
private async Task<string> ExecuteSerperWebSearch(JsonElement input, CancellationToken ct)
{
var apiKey = _config["SerperApiKey"] ?? "";
if (string.IsNullOrWhiteSpace(apiKey))
return "Fehler: Kein SerperApiKey konfiguriert. Bitte in den Server-Einstellungen hinterlegen.";
var query = input.GetProperty("query").GetString() ?? "";
if (string.IsNullOrWhiteSpace(query))
return "Fehler: Suchbegriff (query) darf nicht leer sein.";
var num = input.TryGetProperty("num", out var numProp) ? Math.Clamp(numProp.GetInt32(), 1, 100) : 10;
try
{
var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Add("X-API-KEY", apiKey);
var requestBody = JsonSerializer.Serialize(new { q = query, num = num });
var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://google.serper.dev/search", content, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
return $"Serper API Fehler {(int)response.StatusCode}: {errorBody}";
}
var json = await response.Content.ReadAsStringAsync(ct);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var sb = new StringBuilder();
// Knowledge Graph (wenn vorhanden)
if (root.TryGetProperty("knowledgeGraph", out var kg))
{
if (kg.TryGetProperty("title", out var kgTitle))
sb.AppendLine($"## {kgTitle.GetString()}");
if (kg.TryGetProperty("description", out var kgDesc))
sb.AppendLine(kgDesc.GetString());
sb.AppendLine();
}
// Answer Box (wenn vorhanden)
if (root.TryGetProperty("answerBox", out var ab))
{
sb.AppendLine("### Direkte Antwort");
if (ab.TryGetProperty("title", out var abTitle))
sb.AppendLine($"**{abTitle.GetString()}**");
if (ab.TryGetProperty("answer", out var abAnswer))
sb.AppendLine(abAnswer.GetString());
else if (ab.TryGetProperty("snippet", out var abSnippet))
sb.AppendLine(abSnippet.GetString());
sb.AppendLine();
}
// Organische Ergebnisse
if (root.TryGetProperty("organic", out var organic))
{
var results = organic.EnumerateArray().ToArray();
sb.AppendLine($"**{results.Length} Ergebnisse:**");
sb.AppendLine();
var index = 0;
foreach (var item in results)
{
index++;
var title = item.TryGetProperty("title", out var t) ? t.GetString() : "";
var link = item.TryGetProperty("link", out var l) ? l.GetString() : "";
var snippet = item.TryGetProperty("snippet", out var s) ? s.GetString() : "";
sb.AppendLine($"### {index}. {title}");
sb.AppendLine($"🔗 {link}");
if (!string.IsNullOrEmpty(snippet))
sb.AppendLine(snippet);
sb.AppendLine();
}
}
if (sb.Length == 0)
sb.AppendLine("Keine Ergebnisse gefunden.");
return sb.ToString();
}
catch (TaskCanceledException)
{
return "Serper-Suche abgebrochen (Timeout).";
}
catch (Exception ex)
{
return $"Serper-Suche fehlgeschlagen: {ex.Message}";
}
}
}