Class: RubyLLM::SemanticCache::Scoped

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_llm/semantic_cache/scoped.rb

Overview

Scoped cache wrapper for multi-tenant scenarios Each scoped instance maintains its own stores for true isolation

Examples:

support = RubyLLM::SemanticCache::Scoped.new(namespace: "support")
sales = RubyLLM::SemanticCache::Scoped.new(namespace: "sales")

support.store(query: "How to reset password?", response: "...")
sales.store(query: "What is the price?", response: "...")

Defined Under Namespace

Classes: ScopedConfig

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(namespace:) ⇒ Scoped



18
19
20
21
22
23
24
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 18

def initialize(namespace:)
  @namespace = namespace
  @vector_store = nil
  @cache_store = nil
  @hits = 0
  @misses = 0
end

Instance Attribute Details

#namespaceObject (readonly)

Returns the value of attribute namespace.



16
17
18
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 16

def namespace
  @namespace
end

Instance Method Details

#clear!Object



119
120
121
122
123
124
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 119

def clear!
  vector_store.clear!
  cache_store.clear!
  @hits = 0
  @misses = 0
end

#delete(query, threshold: nil) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 89

def delete(query, threshold: nil)
  threshold ||= config.similarity_threshold
  embedding = embedding_generator.generate(query)
  matches = vector_store.search(embedding, limit: 1)

  return false unless matches.any? && matches.first[:similarity] >= threshold

  id = matches.first[:id]
  vector_store.delete(id)
  cache_store.delete(id)
  true
end

#exists?(query, threshold: nil) ⇒ Boolean



82
83
84
85
86
87
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 82

def exists?(query, threshold: nil)
  threshold ||= config.similarity_threshold
  embedding = embedding_generator.generate(query)
  matches = vector_store.search(embedding, limit: 1)
  matches.any? && matches.first[:similarity] >= threshold
end

#fetch(query, threshold: nil, ttl: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 26

def fetch(query, threshold: nil, ttl: nil, &block)
  raise ArgumentError, "Block required" unless block_given?

  threshold ||= config.similarity_threshold
  ttl ||= config.ttl_seconds

  embedding = embedding_generator.generate(query)
  matches = vector_store.search(embedding, limit: 1)

  if matches.any? && matches.first[:similarity] >= threshold
    @hits += 1
    entry_data = cache_store.get(matches.first[:id])
    return Serializer.deserialize(entry_data[:response]) if entry_data
  end

  @misses += 1
  response = block.call

  store(query: query, response: response, embedding: embedding, ttl: ttl)
  response
end

#invalidate(query, threshold: nil, limit: 100) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 102

def invalidate(query, threshold: nil, limit: 100)
  threshold ||= config.similarity_threshold
  embedding = embedding_generator.generate(query)
  matches = vector_store.search(embedding, limit: limit)

  count = 0
  matches.each do |match|
    next unless match[:similarity] >= threshold

    vector_store.delete(match[:id])
    cache_store.delete(match[:id])
    count += 1
  end

  count
end

#search(query, limit: 5) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 65

def search(query, limit: 5)
  embedding = embedding_generator.generate(query)
  matches = vector_store.search(embedding, limit: limit)

  matches.filter_map do |match|
    entry_data = cache_store.get(match[:id])
    next unless entry_data

    {
      query: entry_data[:query],
      response: Serializer.deserialize(entry_data[:response]),
      similarity: match[:similarity],
      metadata: entry_data[:metadata]
    }
  end
end

#statsObject



126
127
128
129
130
131
132
133
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 126

def stats
  {
    hits: @hits,
    misses: @misses,
    hit_rate: hit_rate,
    entries: cache_store.size
  }
end

#store(query:, response:, embedding: nil, metadata: {}, ttl: nil) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 48

def store(query:, response:, embedding: nil, metadata: {}, ttl: nil)
  embedding ||= embedding_generator.generate(query)
  ttl ||= config.ttl_seconds

  entry = Entry.new(
    query: query,
    response: Serializer.serialize(response),
    embedding: embedding,
    metadata: 
  )

  vector_store.add(entry.id, embedding)
  cache_store.set(entry.id, entry.to_h, ttl: ttl)

  entry
end

#wrap(chat, threshold: nil, ttl: nil, on_cache_hit: nil, max_messages: nil) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ruby_llm/semantic_cache/scoped.rb', line 135

def wrap(chat, threshold: nil, ttl: nil, on_cache_hit: nil, max_messages: nil)
  # For scoped wrap, we create a middleware that uses this scoped instance
  ScopedMiddleware.new(
    self,
    chat,
    threshold: threshold,
    ttl: ttl,
    on_cache_hit: on_cache_hit,
    max_messages: max_messages
  )
end