whoosh 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b74890ede8ed95aadeafdd55439b0a267ec521ecb5c84fd879807f29789a9d9a
4
- data.tar.gz: aa04e3dbf492846d8f06fe057ef657a137065a6b8069421c24013a4102b63511
3
+ metadata.gz: 0c8ecbe08e5ecf50f7f1233da3767074a79d9e4164a719e9397be1ac9637efb3
4
+ data.tar.gz: 622f3f889c550ecdff4cfab4f38120f2ce3f14b7f2021be99dae9790c3cfc5f7
5
5
  SHA512:
6
- metadata.gz: 884921ba3182cef85545192b08301ced1d992705621ce0860e55e916da2ece5f8d1b122f676a88e0e7b18dcbb9f271e01e2e46bbd5439962cd476bfb73173078
7
- data.tar.gz: 8dcb16978fa00228a18de47e42cbe29c0e3d8da84c3bf3a6b4b52842ea9ce4cbcc7a5930b1c471c8b2d06065b6793db6574a8ec2392e7f9707e07d73940be0fb
6
+ metadata.gz: 70add9595209f2ff3a1c5c4a52e25a4428149694f4df9debc7cc58dace2d15b77d46dcf1591cb9cd78e7d02654f0402326c58c9e060f1c94897e0011fc2cfab2
7
+ data.tar.gz: 83fb93d35d3b1d446bc99d1f8ee0a21b16f062af81da4f4e737b98cd85acced622fe332ccefb785c5f114fa78131131bdea9f008e109c49291c793c117cea843
data/lib/whoosh/ai/llm.rb CHANGED
@@ -2,15 +2,48 @@
2
2
 
3
3
  module Whoosh
4
4
  module AI
5
+ # Bounded LRU cache. Ruby's Hash preserves insertion order, so we reorder
6
+ # on read (delete+reinsert) and evict the oldest entry when over capacity.
7
+ class LRUCache
8
+ def initialize(max_size)
9
+ @max_size = max_size
10
+ @store = {}
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def [](key)
15
+ @mutex.synchronize do
16
+ return nil unless @store.key?(key)
17
+ value = @store.delete(key)
18
+ @store[key] = value
19
+ end
20
+ end
21
+
22
+ def []=(key, value)
23
+ @mutex.synchronize do
24
+ @store.delete(key) if @store.key?(key)
25
+ @store[key] = value
26
+ @store.shift while @store.size > @max_size
27
+ value
28
+ end
29
+ end
30
+
31
+ def size
32
+ @store.size
33
+ end
34
+ end
35
+
36
+ DEFAULT_MODEL = "claude-sonnet-4-6"
37
+ DEFAULT_CACHE_MAX = 1000
38
+
5
39
  class LLM
6
40
  attr_reader :provider, :model
7
41
 
8
- def initialize(provider: "auto", model: nil, cache_enabled: true)
42
+ def initialize(provider: "auto", model: nil, cache_enabled: true, cache_size: DEFAULT_CACHE_MAX)
9
43
  @provider = provider
10
44
  @model = model
11
45
  @cache_enabled = cache_enabled
12
- @cache = cache_enabled ? {} : nil
13
- @mutex = Mutex.new
46
+ @cache = cache_enabled ? LRUCache.new(cache_size) : nil
14
47
  @ruby_llm = nil
15
48
  end
16
49
 
@@ -32,10 +65,7 @@ module Whoosh
32
65
  temperature: temperature
33
66
  )
34
67
 
35
- # Cache result
36
- if use_cache && @cache
37
- @mutex.synchronize { @cache[cache_key] = result }
38
- end
68
+ @cache[cache_key] = result if use_cache && @cache
39
69
 
40
70
  result
41
71
  end
@@ -60,18 +90,19 @@ module Whoosh
60
90
  end
61
91
  end
62
92
 
63
- # Stream LLM response — yields chunks
93
+ # Stream an LLM response — yields each chunk as ruby_llm produces it.
94
+ # The block receives a RubyLLM::Chunk (a Message subclass with #content).
95
+ # Returns the final response message after the stream completes.
64
96
  def stream(message, model: nil, system: nil, &block)
65
97
  ensure_ruby_llm!
98
+ unless @ruby_llm
99
+ raise Errors::DependencyError, "No LLM provider available. Add 'ruby_llm' to your Gemfile."
100
+ end
66
101
 
67
- messages = [{ role: "user", content: message }]
68
- # Delegate to ruby_llm's streaming interface
69
- if @ruby_llm
70
- # ruby_llm streaming would go here
71
- # For now, fall back to non-streaming
72
- result = chat(message, model: model, system: system, cache: false)
73
- yield result if block_given?
74
- result
102
+ chat = RubyLLM.chat(model: model || @model || DEFAULT_MODEL)
103
+ chat.with_instructions(system) if system
104
+ chat.ask(message) do |chunk|
105
+ block.call(chunk) if block
75
106
  end
76
107
  end
77
108
 
@@ -87,7 +118,7 @@ module Whoosh
87
118
 
88
119
  if @ruby_llm
89
120
  # Use ruby_llm gem
90
- chat = RubyLLM.chat(model: model || "claude-sonnet-4-20250514")
121
+ chat = RubyLLM.chat(model: model || DEFAULT_MODEL)
91
122
  chat.with_instructions(system) if system
92
123
  response = chat.ask(messages.last[:content])
93
124
  response.content
data/lib/whoosh/ai.rb CHANGED
@@ -11,7 +11,8 @@ module Whoosh
11
11
  LLM.new(
12
12
  provider: ai_config["provider"] || "auto",
13
13
  model: ai_config["model"],
14
- cache_enabled: ai_config["cache"] != false
14
+ cache_enabled: ai_config["cache"] != false,
15
+ cache_size: ai_config["cache_size"] || DEFAULT_CACHE_MAX
15
16
  )
16
17
  end
17
18
  end
@@ -17,7 +17,8 @@ module Whoosh
17
17
 
18
18
  def <<(chunk)
19
19
  return if @closed
20
- text = chunk.respond_to?(:text) ? chunk.text : chunk.to_s
20
+ text = extract_text(chunk)
21
+ return self if text.nil? || text.empty?
21
22
  payload = { choices: [{ delta: { content: text } }] }
22
23
  write("data: #{JSON.generate(payload)}\n\n")
23
24
  self
@@ -40,6 +41,21 @@ module Whoosh
40
41
 
41
42
  private
42
43
 
44
+ # ruby_llm chunks expose #content (Message subclass). Older code paths
45
+ # and plain-string yields are also supported.
46
+ def extract_text(chunk)
47
+ return chunk if chunk.is_a?(String)
48
+ if chunk.respond_to?(:content)
49
+ c = chunk.content
50
+ return "" if c.nil?
51
+ return c if c.is_a?(String)
52
+ return c.text if c.respond_to?(:text)
53
+ return c.to_s
54
+ end
55
+ return chunk.text if chunk.respond_to?(:text)
56
+ chunk.to_s
57
+ end
58
+
43
59
  def write(data)
44
60
  @io.write(data)
45
61
  @io.flush if @io.respond_to?(:flush)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whoosh
4
- VERSION = "1.9.0"
4
+ VERSION = "1.10.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whoosh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo