Module: Legion::MCP::EmbeddingIndex

Extended by:
Logging::Helper
Defined in:
lib/legion/mcp/embedding_index.rb

Class Method Summary collapse

Class Method Details

.build_composite(name, description, params) ⇒ Object



125
126
127
128
129
# File 'lib/legion/mcp/embedding_index.rb', line 125

def build_composite(name, description, params)
  parts = [name, '--', description]
  parts << "Params: #{params.join(', ')}" unless params.empty?
  parts.join(' ')
end

.build_from_tool_data(tool_data, embedder: default_embedder) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/legion/mcp/embedding_index.rb', line 10

def build_from_tool_data(tool_data, embedder: default_embedder)
  @embedder = embedder
  mutex.synchronize do
    composites = tool_data.to_h do |tool|
      [tool[:name], build_composite(tool[:name], tool[:description], tool[:params])]
    end

    cached_vectors = bulk_cache_lookup(composites.values)

    uncached_names = composites.keys.reject { |name| cached_vectors.key?(composites[name]) }
    newly_embedded = {}
    uncached_names.each do |name|
      composite = composites[name]
      vector = safe_embed(composite, embedder)
      newly_embedded[composite] = vector if vector
    end

    bulk_cache_store(newly_embedded) unless newly_embedded.empty?

    tool_data.each do |tool|
      composite = composites[tool[:name]]
      vector = cached_vectors[composite] || newly_embedded[composite]
      next unless vector

      index[tool[:name]] = {
        name:           tool[:name],
        composite_text: composite,
        vector:         vector,
        built_at:       Time.now
      }
    end
  end
end

.bulk_cache_lookup(composite_texts) ⇒ Object



105
106
107
108
109
110
111
112
113
# File 'lib/legion/mcp/embedding_index.rb', line 105

def bulk_cache_lookup(composite_texts)
  return {} unless defined?(Legion::Tools::EmbeddingCache) &&
                   Legion::Tools::EmbeddingCache.respond_to?(:bulk_lookup)

  Legion::Tools::EmbeddingCache.bulk_lookup(composite_texts)
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.bulk_cache_lookup')
  {}
end

.bulk_cache_store(composite_to_vector) ⇒ Object



115
116
117
118
119
120
121
122
123
# File 'lib/legion/mcp/embedding_index.rb', line 115

def bulk_cache_store(composite_to_vector)
  return unless defined?(Legion::Tools::EmbeddingCache) &&
                Legion::Tools::EmbeddingCache.respond_to?(:bulk_store)

  Legion::Tools::EmbeddingCache.bulk_store(composite_to_vector)
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.bulk_cache_store')
  nil
end

.cosine_similarity(vec_a, vec_b) ⇒ Object



62
63
64
65
66
67
68
69
# File 'lib/legion/mcp/embedding_index.rb', line 62

def cosine_similarity(vec_a, vec_b)
  dot = vec_a.zip(vec_b).sum { |a, b| a * b }
  mag_a = Math.sqrt(vec_a.sum { |x| x**2 })
  mag_b = Math.sqrt(vec_b.sum { |x| x**2 })
  return 0.0 if mag_a.zero? || mag_b.zero?

  dot / (mag_a * mag_b)
end

.coverageObject



83
84
85
86
87
88
89
90
# File 'lib/legion/mcp/embedding_index.rb', line 83

def coverage
  mutex.synchronize do
    return 0.0 if index.empty?

    with_vectors = index.values.count { |e| e[:vector] }
    with_vectors.to_f / index.size
  end
end

.default_embedderObject



144
145
146
147
148
149
150
151
152
# File 'lib/legion/mcp/embedding_index.rb', line 144

def default_embedder
  return nil unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?

  ->(text) { Legion::LLM.embed(text)[:vector] }
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.default_embedder')
  log.debug("EmbeddingIndex#default_embedder failed: #{e.message}")
  nil
end

.entry(tool_name) ⇒ Object



71
72
73
# File 'lib/legion/mcp/embedding_index.rb', line 71

def entry(tool_name)
  mutex.synchronize { index[tool_name] }
end

.indexObject



97
98
99
# File 'lib/legion/mcp/embedding_index.rb', line 97

def index
  @index ||= {}
end

.mutexObject



101
102
103
# File 'lib/legion/mcp/embedding_index.rb', line 101

def mutex
  @mutex ||= Mutex.new
end

.populated?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/legion/mcp/embedding_index.rb', line 79

def populated?
  mutex.synchronize { !index.empty? }
end

.reset!Object



92
93
94
95
# File 'lib/legion/mcp/embedding_index.rb', line 92

def reset!
  @embedder = nil
  mutex.synchronize { index.clear }
end

.safe_embed(text, embedder) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/legion/mcp/embedding_index.rb', line 131

def safe_embed(text, embedder)
  return nil unless embedder

  result = embedder.call(text)
  return nil unless result.is_a?(Array) && !result.empty?

  result
rescue StandardError => e
  handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.safe_embed')
  log.debug("EmbeddingIndex#safe_embed failed: #{e.message}")
  nil
end

.semantic_match(intent, embedder: @embedder || default_embedder, limit: 5) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/legion/mcp/embedding_index.rb', line 44

def semantic_match(intent, embedder: @embedder || default_embedder, limit: 5)
  return [] if index.empty?

  intent_vec = safe_embed(intent, embedder)
  return [] unless intent_vec

  scores = mutex.synchronize do
    index.values.filter_map do |entry|
      next unless entry[:vector]

      score = cosine_similarity(intent_vec, entry[:vector])
      { name: entry[:name], score: score }
    end
  end

  scores.sort_by { |s| -s[:score] }.first(limit)
end

.sizeObject



75
76
77
# File 'lib/legion/mcp/embedding_index.rb', line 75

def size
  mutex.synchronize { index.size }
end