Class: Pikuri::VectorDb::Tools::Search
- Inherits:
-
Tool
- Object
- Tool
- Pikuri::VectorDb::Tools::Search
- Defined in:
- lib/pikuri/vector_db/tools/search.rb
Overview
The LLM-facing vectordb_search tool — agentic search, the agent
decides when to retrieve. Composes Embedder + Backend + optional
Reranker: embed the query, Backend#query for candidates
(CANDIDATE_K with a reranker to narrow back to FINAL_K, else
FINAL_K directly), optionally rerank (reorder by Reranker::Hit.index,
take top FINAL_K), then format as a numbered source + score +
snippet list capped at SNIPPET_LENGTH chars.
Which score is shown
The score tracks whatever determined the ordering, so it never
disagrees with the rank — but the two modes put different scales behind
one score= label:
- Vector-only (no reranker, or outage fallback) — cosine similarity
[0, 1], comparable across results. - Reranked — the Reranker::Hit relevance score, a cross-encoder value on a model-dependent scale (can be negative/unbounded for raw logits — see Reranker::Hit), a relative gradient not a 0–1 threshold. pikuri shows the one the order was built from rather than a misleading cosine.
A reranker outage falls back, doesn't fail: its Reranker::LlamaServer
RuntimeError is caught, WARN-logged, and the vector-only top FINAL_K
returned — reranker is a quality knob, degraded results beat a tool error.
Single-param surface (+query:+ only, no +top_k:+/+reranker:+ toggle): retrieval depth and rerank choice are host policy baked into Extension, not something the LLM should tune mid-conversation.
Sharing: P_shared_locked, and sharing is what you want — an index is
expensive to build and is the same corpus for every agent, so ten
agents over one Backend is the intended shape. Backend::InMemory
locks; the server backends serialize on their own side; the embedder
and reranker are stateless HTTP.
Constant Summary collapse
- LOGGER =
Pikuri.logger_for('VectorDb::Tools::Search')
- FINAL_K =
Returns results returned to the LLM — the RAG sweet spot: enough for related-but-different angles, few enough to fit a turn.
5- CANDIDATE_K =
Returns over-fetch size with a reranker (retrieve-broad-then- narrow — the cross-encoder sees more candidates than vector search surfaces and reorders by query-conditional relevance).
50- SNIPPET_LENGTH =
Returns per-result text cap in the observation (~2.5 KB for a five-result page; the agent can re-query if a snippet cuts off).
500- DESCRIPTION =
Returns static description shown to the LLM, opencode-shape (summary +
Usage:bullets). <<~DESC Search the indexed document corpus for content relevant to a query. Usage: - Use when the user asks about facts, definitions, or topics that would live in their indexed documents (notes, docs, knowledge base). - Phrase the query as a complete question or topic statement — the embedder reads natural language, not keyword bags. - Returns up to #{FINAL_K} results with `source` paths and text snippets; cite results back using the source path. - If a query returns nothing useful, either the relevant doc isn't in the corpus, or the corpus hasn't been indexed yet — say which you suspect, and offer to run `vectordb_reindex` to build the index. DESC
Class Method Summary collapse
-
.execute(embedder:, backend:, reranker:, query:) ⇒ String
Public so specs can exercise the search pipeline without constructing a Tool wrapper.
Instance Method Summary collapse
Constructor Details
#initialize(embedder:, backend:, reranker: nil) ⇒ Search
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/pikuri/vector_db/tools/search.rb', line 77 def initialize(embedder:, backend:, reranker: nil) super( name: 'vectordb_search', description: DESCRIPTION, parameters: Pikuri::Tool::Parameters.build { |p| p.required_string :query, 'Natural-language search query, e.g. ' \ '"how does the deployment pipeline handle migrations?" or ' \ '"recipe with mushrooms and risotto rice".' }, execute: lambda { |query:| Search.execute( embedder: , backend: backend, reranker: reranker, query: query ) }, # Hard untrusted: a corpus is a pile of documents pikuri did not # write, and web-sourced corpora are the common case. No egress — # the query never leaves the machine, which is the whole point of # local retrieval. # # No private leg: there is no per-corpus declaration yet, so this # stays the quiet default rather than guessing. A private corpus is # under-warned; see +private:+ on {Pikuri::Workspace::Filesystem} # for the shape it would take. trifecta_legs: Pikuri::Tool::TrifectaLegs.new(private: false, untrusted: :hard, egress_payload_review: :no_egress) ) end |
Class Method Details
.execute(embedder:, backend:, reranker:, query:) ⇒ String
Public so specs can exercise the search pipeline without constructing a Tool wrapper.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/pikuri/vector_db/tools/search.rb', line 114 def self.execute(embedder:, backend:, reranker:, query:) return 'Error: query is empty' if query.nil? || query.strip.empty? candidate_k = reranker ? CANDIDATE_K : FINAL_K query_vector = .([query]).first candidates = backend.query(vector: query_vector, top_k: candidate_k) return 'No matches found in the indexed corpus.' if candidates.empty? final = if reranker reranked_or_fallback(reranker: reranker, query: query, candidates: candidates) else candidates end.first(FINAL_K) format_observation(final) end |