Class: Phronomy::Memory::Retrieval::Semantic

Inherits:
Base
  • Object
show all
Defined in:
lib/phronomy/memory/retrieval/semantic.rb

Overview

Retrieval strategy that returns the k semantically closest messages to the query.

Messages are indexed in a VectorStore on save. On retrieval, the query is embedded and the k nearest messages are returned. Falls back to the k most recent messages when no query is provided.

Examples:

retrieval = Phronomy::Memory::Retrieval::Semantic.new(
  embeddings: Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small"),
  k: 10
)

Instance Method Summary collapse

Constructor Details

#initialize(embeddings:, store: nil, k: 10, max_index_size: nil) ⇒ Semantic

Returns a new instance of Semantic.

Parameters:

  • store (Phronomy::VectorStore::Base) (defaults to: nil)

    vector store (default InMemory)

  • embeddings (Phronomy::Embeddings::Base)

    embeddings adapter

  • k (Integer) (defaults to: 10)

    number of messages to retrieve

  • max_index_size (Integer, nil) (defaults to: nil)

    maximum number of entries kept in the local index. When nil, the index grows unboundedly. When exceeded, the oldest entries (by insertion order) are evicted.



24
25
26
27
28
29
30
31
32
# File 'lib/phronomy/memory/retrieval/semantic.rb', line 24

def initialize(embeddings:, store: nil, k: 10, max_index_size: nil)
  @store = store || Phronomy::VectorStore::InMemory.new
  @embeddings = embeddings
  @k = k
  @index = {}   # id => message  (insertion-ordered via Ruby Hash)
  @counter = 0
  @max_index_size = max_index_size
  @mutex = Mutex.new
end

Instance Method Details

#clear_index(thread_id:) ⇒ Object

Clear indexed messages for a thread.

Parameters:

  • thread_id (String)


55
56
57
58
59
60
61
62
63
# File 'lib/phronomy/memory/retrieval/semantic.rb', line 55

def clear_index(thread_id:)
  @mutex.synchronize do
    ids = @index.keys.select { |id| id.start_with?("#{thread_id}:") }
    ids.each do |id|
      @index.delete(id)
      @store.remove(id: id)
    end
  end
end

#index(thread_id:, messages:) ⇒ Object

Index a new batch of messages so they are searchable on future #select calls. Called by ConversationManager#save.

Parameters:

  • thread_id (String)
  • messages (Array)


39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/phronomy/memory/retrieval/semantic.rb', line 39

def index(thread_id:, messages:)
  messages.each do |msg|
    embedding = @embeddings.embed(msg.content.to_s)
    @mutex.synchronize do
      id = "#{thread_id}:#{@counter}"
      @counter += 1
      @store.add(id: id, embedding: embedding, metadata: {thread_id: thread_id, message: msg})
      @index[id] = msg
      evict_oldest! if @max_index_size && @index.size > @max_index_size
    end
  end
end

#select(messages, query: nil, thread_id: nil) ⇒ Array

Return semantically relevant messages, or recent messages when query is nil.

Parameters:

  • messages (Array)

    full history (used as fallback when query is nil)

  • query (String, nil) (defaults to: nil)

    current user input for semantic search

  • thread_id (String, nil) (defaults to: nil)

    when provided, results are filtered to this thread

Returns:

  • (Array)


71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/phronomy/memory/retrieval/semantic.rb', line 71

def select(messages, query: nil, thread_id: nil)
  if query && !query.strip.empty?
    query_embedding = @embeddings.embed(query)
    results = @store.search(query_embedding: query_embedding, k: @k * 3)
    results
      .select { |r| thread_id.nil? || r[:metadata][:thread_id] == thread_id }
      .first(@k)
      .map { |r| r[:metadata][:message] }
  else
    messages.last(@k)
  end
end