Class: Kward::WebSearch

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/tools/search/web.rb

Overview

Live web-search implementation with provider fallbacks.

Defined Under Namespace

Classes: NetHttpClient, Result, SearchResponse

Constant Summary collapse

DEFAULT_MAX_RESULTS =
5
MAX_MAX_RESULTS =
20
MAX_QUERIES =
4
MAX_OUTPUT_BYTES =
8 * 1024
MODEL_PROVIDER_MAX_TOKENS =
512
MAX_ANSWER_CHARS =
2_000
MAX_EXCERPT_CHARS =
300
HTTP_TIMEOUT_SECONDS =
10
DUCKDUCKGO_URL =
"https://html.duckduckgo.com/html/"
EXA_MCP_URL =
"https://mcp.exa.ai/mcp"
EXA_ANSWER_URL =
"https://api.exa.ai/answer"
EXA_SEARCH_URL =
"https://api.exa.ai/search"
PERPLEXITY_API_URL =
"https://api.perplexity.ai/chat/completions"
GEMINI_API_BASE =
"https://generativelanguage.googleapis.com/v1beta"
DEFAULT_GEMINI_MODEL =
"gemini-2.5-flash"
PUBLIC_SEARXNG_INSTANCES =
[
  "https://searx.be",
  "https://search.inetol.net",
  "https://searx.tiekoetter.com"
].freeze
PROVIDERS =
%w[auto exa perplexity gemini duckduckgo].freeze

Instance Method Summary collapse

Constructor Details

#initialize(http_client: NetHttpClient.new, searxng_instances: PUBLIC_SEARXNG_INSTANCES, max_output_bytes: MAX_OUTPUT_BYTES, config: nil) ⇒ WebSearch

Creates an object for web search provider operations.



38
39
40
41
42
43
# File 'lib/kward/tools/search/web.rb', line 38

def initialize(http_client: NetHttpClient.new, searxng_instances: PUBLIC_SEARXNG_INSTANCES, max_output_bytes: MAX_OUTPUT_BYTES, config: nil)
  @http_client = http_client
  @searxng_instances = searxng_instances
  @max_output_bytes = max_output_bytes
  @config = config
end

Instance Method Details

#available?Boolean

Returns:

  • (Boolean)


45
46
47
48
49
50
# File 'lib/kward/tools/search/web.rb', line 45

def available?
  enabled = boolean_config_value("enabled")
  return enabled unless enabled.nil?

  true
end

#search(args) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/kward/tools/search/web.rb', line 52

def search(args)
  queries = args_value(args, "queries")
  return "Error: queries must be an array with 1-#{MAX_QUERIES} strings" unless valid_queries?(queries)

  max_results = bounded_max_results(args_value(args, "max_results") || args_value(args, "num_results"))
  provider = normalize_provider(args_value(args, "provider") || config_value("provider") || "auto")
  return "Error: provider must be one of: #{PROVIDERS.join(", ")}" unless provider

  options = {
    max_results: max_results,
    recency_filter: normalize_recency(args_value(args, "recency_filter") || args_value(args, "recencyFilter")),
    domain_filter: normalize_domain_filter(args_value(args, "domain_filter") || args_value(args, "domainFilter")),
    provider: provider
  }

  sections = ["# Web search"]
  failures = []
  any_results = false

  queries.each do |query|
    response, error = search_query(query, options)
    any_results = true if successful_response?(response)
    failures << "#{query}: #{error}" if error && !successful_response?(response)
    sections << format_query_results(query, response, error)
  end

  unless any_results
    return "Error: web_search found no results\n#{failures.map { |failure| "- #{failure}" }.join("\n")}".strip
  end

  truncate_output(sections.join("\n\n"))
end