Class: Insika::ToolCatalog

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/tool_catalog.rb

Overview

"Level 1" (progressive disclosure) view of the tools: just name+description, the analog of SkillCatalog for tools. It does NOT read disk (the tools are already in the ToolRegistry, registered at boot) and does NOT inherit from RubyLLM::Tool — duck typing over .description (pure, testable without the gem).

The canonical description does not live in the Entry (Registry::Entry has no such field): it comes from the tool INSTANCE (factory.call.description). That is why the catalog is LAZY: it only instantiates the tools on the first query (all), not at boot. A deployment without tools_deferred never touches the catalog and pays for no instantiation at all; a broken factory surfaces on first use (where the Executor would also catch it at stage 3), not at construction.

Defined Under Namespace

Classes: Entry

Instance Method Summary collapse

Constructor Details

#initialize(tool_registry:) ⇒ ToolCatalog

Returns a new instance of ToolCatalog.



19
20
21
# File 'lib/insika/tool_catalog.rb', line 19

def initialize(tool_registry:)
  @tool_registry = tool_registry
end

Instance Method Details

#allObject



23
24
25
# File 'lib/insika/tool_catalog.rb', line 23

def all
  @entries ||= build_entries
end

#format_for_prompt(entries = all) ⇒ Object

Level 1 injected into the prompt — mirror of SkillCatalog#format_for_prompt, swapping the tag and the final instruction (load_skill -> tool_search).



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/insika/tool_catalog.rb', line 60

def format_for_prompt(entries = all)
  return "" if entries.empty?

  lines = entries.map { |e| %(  <tool name="#{e.name}">#{e.description}</tool>) }.join("\n")

  <<~PROMPT.strip
    <available_tools>
    #{lines}
    </available_tools>

    Before using a tool above, call `tool_search` with what you need
    to do to enable it in this conversation.
  PROMPT
end

#reloadObject

Reloads the index (after authoring a data-tool in the overlay). Mirrors SkillCatalog#reload — level-1/tool_search starts seeing the new tool without a restart. An in-flight turn has already captured all.



30
31
32
33
# File 'lib/insika/tool_catalog.rb', line 30

def reload
  @entries = build_entries
  self
end

#search(query, within: nil) ⇒ Object

PURE matcher: case-insensitive, substring/keyword, NO embeddings. name weighs 2, description weighs 1; ties broken by original index (Ruby's sort_by is not stable). within: restricts the universe via subset.



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/insika/tool_catalog.rb', line 46

def search(query, within: nil)
  terms = query.to_s.downcase.split(/\s+/).reject(&:empty?)
  return [] if terms.empty?

  universe = within ? subset(within) : all
  scored = universe.each_with_index.filter_map do |entry, idx|
    score = score_entry(entry, terms)
    [entry, score, idx] if score.positive?
  end
  scored.sort_by { |_entry, score, idx| [-score, idx] }.map(&:first)
end

#subset(names) ⇒ Object

Allowed deferred slice (typically allowed_tools ∩ tools_deferred). Names outside the catalog are silently ignored (safe failure: less exposure, never more).



38
39
40
41
# File 'lib/insika/tool_catalog.rb', line 38

def subset(names)
  wanted = Array(names).map(&:to_s)
  all.select { |e| wanted.include?(e.name) }
end