run method
Executes the tool with the given input map.
Should never throw — return ToolResult.failure on errors.
Implementation
@override
Future<ToolResult> run(Map<String, dynamic> input) async {
final query = (input['query'] as String? ?? '').trim();
if (query.isEmpty) {
return ToolResult.failure(name, 'Input "query" must not be empty.');
}
_logger.info('Searching: "$query"');
// Return mock results if provided (testing / offline)
if (_mockResults != null) {
final results = _mockResults!
.map((r) => '• ${r["title"] ?? ""}: ${r["snippet"] ?? ""}')
.join('\n');
return ToolResult.success(name, results,
metadata: {'query': query, 'source': 'mock'});
}
// Real DuckDuckGo Instant Answers (JSON API, no key needed)
try {
final uri = Uri.parse(searchEngineUrl).replace(queryParameters: {
'q': query,
'format': 'json',
'no_html': '1',
'skip_disambig': '1',
});
final response = await _client
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
return ToolResult.failure(
name,
'Search API returned HTTP ${response.statusCode}',
);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final abstract_ = (json['AbstractText'] as String? ?? '').trim();
final relatedTopics = (json['RelatedTopics'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.take(3)
.map((t) => (t['Text'] as String? ?? '').trim())
.where((t) => t.isNotEmpty)
.join('\n');
final output = [
if (abstract_.isNotEmpty) 'Summary: $abstract_',
if (relatedTopics.isNotEmpty) 'Related:\n$relatedTopics',
].join('\n\n');
return ToolResult.success(
name,
output.isEmpty ? 'No results found for "$query".' : output,
metadata: {'query': query, 'source': 'duckduckgo'},
);
} catch (e) {
_logger.warning('Search failed: $e');
return ToolResult.failure(name, 'Search error: $e');
}
}