ollama_chat 0.0.133 → 0.0.135

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: 9587b954a06f8bea7be16013029fb1c8464ae82ff49e5f386f9be8636383d33e
4
- data.tar.gz: 68a5dad823096507c3b718442a45412b113a5dd13bb5affb7bcd5714f97fd337
3
+ metadata.gz: a7f6ef35b1ea750d80dfbfd13c36ac48abeb22ecbddf53abdb50974108eea0d5
4
+ data.tar.gz: 3cde04bc79e939681722cc75277f8fbc59e3306848bc6f6afbca28c5ed408724
5
5
  SHA512:
6
- metadata.gz: 8d120d00e82bd94211b6cd7025032624191f1d2d70362715a0896712451f219711207cda98b095144c1ea33d3cde1e5f31ccaa6009087c963ece42a3a74bdf9b
7
- data.tar.gz: 9ad6d00fc1d244ee9b4ff4f30cf5c7fdad3893c74a69c96e1599dc082c065955f440bfe4218d170e85a1e266d02341d94ad57a7a51f8a24a795f91aedd3a4b0a
6
+ metadata.gz: 139f770de1fc5401880978f540a6a9a0af131e69fbf5d67214d57e5798f2df8bc0321a65dd5830db50f48533c9ba040e057d6ed731a6d59dfbb77404b0f1f0c6
7
+ data.tar.gz: 33c1d7391407ec800b8717c342ef334aaa2eeb15c0262e9dc7ef1c284181f56d8d0d5c80b3fba1f474cbdfd447522b76d2d3dc0e7207442dc1d68a0f36d0bd8d
data/CHANGES.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-19 v0.0.135
4
+
5
+ ### Features
6
+
7
+ - Introduced `OllamaChat::ModelHandling::ModelCache` to manage model metadata
8
+ and sizes with two-tier storage (names/sizes and full metadata), supporting
9
+ lazy registration and per-client isolation.
10
+ - Added `-r` flag to the `/model change` command to flush the model cache
11
+ before re-listing models.
12
+ - Added `OllamaChat::ToolCallProcessor` to encapsulate the tool call lifecycle,
13
+ including execution, result storage, and draining of results for the chat
14
+ loop.
15
+ - Enabled searching sessions by ID in the session chooser by including the
16
+ session ID in the search value.
17
+
18
+ ### Fixes
19
+
20
+ - Rescued `Ollama::Errors::BadRequestError` in `model_present?` to prevent
21
+ unhandled exceptions for model names rejected by the server with a 400
22
+ status.
23
+ - Fixed persona leakage in new sessions by using `config.persona&.default_name`
24
+ instead of the current session's persona in `Session.with_defaults`.
25
+ - Synchronized session triggers with the active persona via `sync_trigger` in
26
+ `PersonaeManagement`, ensuring triggers are rekeyed, cleared, or seeded
27
+ correctly on persona changes.
28
+
29
+ ### Improvements
30
+
31
+ - Refactored `model_present?` to use the new `ModelCache` instead of an inline
32
+ hash, improving cache management and reducing repeated API calls.
33
+ - Updated `choose_model` to skip the `all_models` API call if the requested
34
+ model is already confirmed present via `model_present?`.
35
+ - Renamed the prompt context for report templates from `session` to `report` in
36
+ `default_config.yml` and `SessionManagement` to better reflect its purpose.
37
+ - Renamed `OllamaChat::Database::SessionLocking` to
38
+ `OllamaChat::Database::RowLocking` to reflect its generic row-level locking
39
+ behavior, removing the `chat:` keyword argument from `lock?` and moving
40
+ feedback emission to the caller.
41
+ - Replaced `Kramdown::ANSI::Width.truncate` and `Kramdown::ANSI::Width.wrap`
42
+ with bare `truncate` and `wrap` in multiple modules where ANSI sequences are
43
+ not present at measurement time.
44
+ - Truncated session names in the `/sessions` list to fit terminal width,
45
+ preventing long names from wrapping or pushing metadata off-screen.
46
+ - Decoupled `model_with_size` from the `Ollama::Tag` shape, now accepting model
47
+ name and size directly.
48
+
49
+ ### Build & CI
50
+
51
+ - Updated `gem_hadar` development dependency to **2.20.1**.
52
+ - Regenerated gem metadata to include new files (`tool_call_processor.rb`,
53
+ `model_cache.rb`, `row_locking.rb`) and updated file lists.
54
+
55
+ ### Testing
56
+
57
+ - Added `spec/ollama_chat/model_handling/model_cache_spec.rb` with 18 examples
58
+ covering cache operations, lazy registration, per-instance isolation, and
59
+ flushing.
60
+ - Added `spec/ollama_chat/personae_management_spec.rb` with 5 examples covering
61
+ `sync_trigger` state transitions.
62
+ - Added `spec/ollama_chat/tool_call_processor_spec.rb` covering `drain`,
63
+ `clear!`, and result storage.
64
+ - Updated `spec/ollama_chat/commands_spec.rb` to test the `-r` flag for `/model
65
+ change`.
66
+ - Updated `spec/ollama_chat/session_management_spec.rb` to verify persona and
67
+ trigger behavior in new and duplicated sessions, and adjusted lock
68
+ expectations.
69
+ - Updated `spec/ollama_chat/information_spec.rb` to reflect the correct persona
70
+ display after initialization.
71
+ - Removed `#handle_tool_call_results?` tests from
72
+ `spec/ollama_chat/tool_calling_spec.rb` as the method was moved to
73
+ `ToolCallProcessor`.
74
+
75
+ ## 2026-10-17 v0.0.134
76
+
77
+ * Removed the local dependency on the `ollama-ruby` gem from the `Gemfile`.
78
+
3
79
  ## 2026-10-17 v0.0.133
4
80
 
5
81
  ### Features
data/Gemfile CHANGED
@@ -3,5 +3,3 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
-
7
- gem 'ollama-ruby', path: '../ollama-ruby'
@@ -116,6 +116,7 @@ class OllamaChat::Chat
116
116
  setup_switches
117
117
  setup_session
118
118
  setup_state_selectors(config)
119
+ @model_cache = ModelCache.new
119
120
  connect_ollama
120
121
  connect_embedding_ollama
121
122
  @documents = setup_documents
@@ -123,7 +124,7 @@ class OllamaChat::Chat
123
124
  @cache = setup_cache
124
125
  @images = []
125
126
  @kramdown_ansi_styles = configure_kramdown_ansi_styles
126
- @tool_call_results = Hash.new { |h, name| h[name] = [] }
127
+ @tool_call_processor = OllamaChat::ToolCallProcessor.new(chat: self)
127
128
  setup_personae_directory
128
129
  @opts[?S] and init_server_socket
129
130
  rescue ComplexConfig::AttributeMissing, ComplexConfig::ConfigurationSyntaxError => e
@@ -166,6 +167,11 @@ class OllamaChat::Chat
166
167
  # @return [OllamaChat::RedisCache, nil] the cache instance
167
168
  attr_reader :cache
168
169
 
170
+ # The model_cache method provides read-only access to the model cache.
171
+ #
172
+ # @return [ ModelCache ] the model cache instance
173
+ attr_reader :model_cache
174
+
169
175
  # Returns the voice handler class based on the configuration.
170
176
  #
171
177
  # This method resolves the voice handler class specified in the config.
@@ -341,6 +347,12 @@ class OllamaChat::Chat
341
347
  "#<#{self.class}: #{base_url.to_s}>"
342
348
  end
343
349
 
350
+ # Returns the tool call processor instance.
351
+ #
352
+ # @return [OllamaChat::ToolCallProcessor] the processor that handles
353
+ # tool execution and result storage
354
+ attr_reader :tool_call_processor
355
+
344
356
  private
345
357
 
346
358
  # Wrapper for ollama.chat to provide consistent logging of all arguments.
@@ -462,7 +474,7 @@ class OllamaChat::Chat
462
474
  type = :terminal_input
463
475
  input_prompt = bold { color(172) { message_type(@images) + " user" } } + bold { "> " }
464
476
  begin
465
- tools_were_called = handle_tool_call_results? { |index, tool_name, content|
477
+ tools_were_called = tool_call_processor.drain { |index, tool_name, content|
466
478
  messages << OllamaChat::Message.new(
467
479
  role: 'user', # XXX this should be 'tool' but it doesn't currently seem to work in Ollama API
468
480
  sender_name: tool_name,
@@ -552,12 +552,12 @@ module OllamaChat::Commands
552
552
 
553
553
  command(
554
554
  name: :model,
555
- regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-p\s+[\S]+))*)$),
555
+ regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-r|-p\s+[\S]+))*)$),
556
556
  complete: [ 'model', %w[ change options options\ copy options\ delete options\ export options\ import options\ from\ session options\ to\ session ] ],
557
- options: '[-m|-p pattern]',
557
+ options: '[-m|-r|-p pattern]',
558
558
  help: <<~EOT
559
559
  🤖 Manage AI models & profiles:
560
- - change: Switch active model
560
+ - change: Switch active model (-r to refresh)
561
561
  - options: Edit saved profile config
562
562
  - options copy: Copy profile from another model
563
563
  - options delete: Delete a saved profile
@@ -565,11 +565,14 @@ module OllamaChat::Commands
565
565
  - options import: Selectively import profiles from JSON
566
566
  - options from session: Save live → Saved
567
567
  - options to session: Apply Saved → Live
568
- -m interactively choose a model
569
- -p PATTERN narrow file search for import
568
+ -m interactively choose a model
569
+ -r refresh model list (flush cache)
570
+ -p PATTERN narrow file search for import
570
571
  EOT
571
572
  ) do |subcommand, opts|
572
573
  if subcommand == 'change'
574
+ opts = go_command('mr', opts)
575
+ @model_cache.flush if opts[?r]
573
576
  begin
574
577
  model = choose_model('', @model)
575
578
  profile = choose_profile_for_model(model) || 'default'
@@ -5,7 +5,7 @@
5
5
  # the JSON serialization of complex attributes such as `tools_default_enabled`
6
6
  # and `model_options`.
7
7
  class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
8
- include OllamaChat::Database::SessionLocking
8
+ include OllamaChat::Database::RowLocking
9
9
  include OllamaChat::Database::Duplicatable
10
10
 
11
11
  plugin :timestamps, update_on_create: true
@@ -200,7 +200,7 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
200
200
  name: ,
201
201
  current_model: ,
202
202
  current_collection: chat.initial_collection,
203
- default_persona_name: chat.initial_persona_name,
203
+ default_persona_name: chat.config.persona&.default_name,
204
204
  current_system_prompt: chat.initial_system_prompt,
205
205
  tools_enabled: chat.config.tools.enabled,
206
206
  tools_default_enabled: ,
@@ -1,15 +1,15 @@
1
- # The SessionLocking module provides a mechanism for exclusive session access,
1
+ # The RowLocking module provides a mechanism for exclusive row access,
2
2
  # similar to the swap-file locking used by Vim.
3
3
  #
4
4
  # It prevents multiple concurrent instances of the application from modifying
5
- # the same session, thereby avoiding race conditions and context collision.
5
+ # the same database row, thereby avoiding race conditions and state collision.
6
6
  # The lock is based on the Process ID (PID) and is validated by checking if
7
7
  # the owning process is still active on the system.
8
8
  #
9
- # This module is intended to be mixed into models that persist session state,
10
- # such as OllamaChat::Database::Models::Session.
11
- module OllamaChat::Database::SessionLocking
12
- # Checks if the session is currently locked by an active process.
9
+ # This module is intended to be mixed into any Sequel model that has a
10
+ # `locked_by_pid` column.
11
+ module OllamaChat::Database::RowLocking
12
+ # Checks if the row is currently locked by an active process.
13
13
  #
14
14
  # @return [Integer, nil] the PID of the locking process if active, or nil if unlocked/stale.
15
15
  def locked?
@@ -23,17 +23,12 @@ module OllamaChat::Database::SessionLocking
23
23
  end
24
24
  end
25
25
 
26
- # Attempts to acquire a lock on the session.
26
+ # Attempts to acquire a lock on the row.
27
27
  #
28
- # @param chat [OllamaChat] the chat instance that receives feedback
29
28
  # @return [Boolean] true if the lock was successfully acquired, false if already locked.
30
- def lock?(chat:)
31
- if running_pid = locked?
32
- chat.feedback("session #{id} locked by running process #{running_pid}", type: :warn)
33
- false
34
- else
35
- lock
36
- end
29
+ def lock?
30
+ return false if locked?
31
+ !!lock
37
32
  end
38
33
 
39
34
  # Sets the lock for the current process.
@@ -43,7 +38,7 @@ module OllamaChat::Database::SessionLocking
43
38
  update(locked_by_pid: $$)
44
39
  end
45
40
 
46
- # Releases the lock on the session.
41
+ # Releases the lock on the row.
47
42
  #
48
43
  # @return [Boolean] true if the update was successful.
49
44
  def unlock
@@ -51,4 +51,4 @@ module OllamaChat::Database
51
51
  end
52
52
  end
53
53
  require 'ollama_chat/database/duplicatable'
54
- require 'ollama_chat/database/session_locking'
54
+ require 'ollama_chat/database/row_locking'
@@ -89,147 +89,13 @@ class OllamaChat::FollowChat
89
89
 
90
90
  output_eval_stats(response)
91
91
 
92
- handle_tool_calls(response)
92
+ chat.tool_call_processor.handle(response)
93
93
 
94
94
  self
95
95
  end
96
96
 
97
97
  private
98
98
 
99
- # The handle_tool_calls method processes tool calls from a response and
100
- # executes them.
101
- #
102
- # This method checks if the response contains tool calls, and if so, iterates
103
- # through each tool call to execute the corresponding tool from the
104
- # registered tools. The results of the tool execution are stored in the
105
- # chat's tool_call_results hash using the tool name as the key.
106
- #
107
- # @param response [Object] the response object containing tool calls to
108
- # process
109
- def handle_tool_calls(response)
110
- return unless response.message.ask_and_send(:tool_calls)
111
-
112
- tools_used = {}
113
-
114
- response.message.tool_calls.each do |tool_call|
115
- name = tool_call.function.name
116
- if %r(.*[/:](?<new_name>.+)) =~ name
117
- msg = "Received namespaced tool call for #{name}, correcting to #{new_name}"
118
- chat.log(:warn, msg, data: { tool: name, new_name: })
119
- name = new_name
120
- end
121
- unless chat.tool_configured?(name)
122
- msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name
123
- chat.tool_call_results[name] << msg
124
- chat.log(:error, msg, data: { tool: name, reason: :unconfigured })
125
- next
126
- end
127
- unless chat.tool_registered?(name)
128
- msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name
129
- chat.tool_call_results[name] << msg
130
- chat.log(:error, msg, data: { tool: name, reason: :unregistered })
131
- next
132
- end
133
- unless chat.tool_enabled?(name)
134
- msg = "Error: Disabled tool named %s ignored => Skip.\n" % name
135
- chat.tool_call_results[name] << msg
136
- chat.log(:error, msg, data: { tool: name, reason: :disabled })
137
- next
138
- end
139
- feedback(?\n, output:)
140
- confirmed = :implicit
141
- resolve = nil
142
- function = JSON.pretty_generate(tool_call.function)
143
- chat.log(:info, "Tool call received", data: { tool: name, function: })
144
- if chat.tool_function(name).require_confirmation?
145
- feedback(
146
- ("I want to execute tool %s\n%s\n" % [ bold { name }, italic { function }, ]),
147
- type: :alarm
148
- )
149
- prompt = '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
150
- answer = chat.confirm?(prompt:)&.to_s&.downcase
151
- resolve = 'You **MUST** ask the user for instructions on how to proceed!!!'
152
- case answer
153
- when 'y'
154
- confirmed = :explicit
155
- when 'n'
156
- confirmed = :denied
157
- else
158
- confirmed = :denied
159
- instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
160
- resolve = instr.full? || resolve
161
- end
162
- else
163
- feedback(
164
- "Executing tool %s\n%s" % [ bold { name }, italic { function }, ]
165
- )
166
- end
167
- start = Time.now
168
- result = nil
169
- case confirmed
170
- when :denied
171
- result = JSON(message: 'User denied confirmation!', resolve:)
172
- feedback(?\n)
173
- feedback(
174
- ("Execution of tool %s denied by user.\n\n" % [ bold { name } ]),
175
- type: :denied
176
- )
177
- chat.log(:warn, "Tool execution denied", data: { tool: name, reason: :denied })
178
- else
179
- symbol = confirmed == :implicit ? '☑️ ' : '✅'
180
- feedback(
181
- ("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ])
182
- )
183
- result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call)
184
- chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
185
- end
186
-
187
- chat.tool_call_results[name] << result
188
- data = nil
189
- message = begin
190
- data = JSON.parse(result)
191
- chat.log(:info, "Tool result", data: { result: data })
192
- data.to_hash['message'] if data.respond_to?(:to_hash)
193
- rescue
194
- chat.log(:info, "Tool result", data: { result: })
195
- nil
196
- end
197
- warn =
198
- begin
199
- !!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false
200
- rescue
201
- false
202
- end
203
-
204
- es = OllamaChat::TokenEstimator.estimate(result.to_s)
205
-
206
- tools_used[name] = {
207
- message:,
208
- warn: ,
209
- size: es.bytes_formatted,
210
- tokens: es.tokens_formatted,
211
- duration: Tins::Duration.new(Time.now - start).to_s,
212
- }
213
- end
214
-
215
- if tools_used.full?
216
- infobar.reset
217
- tools_used.each do |name, info|
218
- feedback_message =
219
- if info[:message]
220
- "\n%s %s\n\n" % [ info[:warn] ? '⚠️' : '💡', info[:message] ]
221
- end
222
- feedback(<<~EOT.strip)
223
- 🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}).
224
- #{feedback_message}
225
- EOT
226
- feedback(?\n)
227
- timeout = chat.tool_function(name).result_display_timeout?
228
- chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:)
229
- end
230
- end
231
- end
232
-
233
99
  # The truncate_for_terminal method processes text to fit within a specified
234
100
  # number of lines.
235
101
  #
@@ -74,7 +74,7 @@ module OllamaChat::Information
74
74
  def collection_stats(output: STDOUT)
75
75
  col = database_collection?(collection)
76
76
  length = (Tins::Terminal.cols - 10).clamp(0..)
77
- wrapped_tags = Kramdown::ANSI::Width.
77
+ wrapped_tags =
78
78
  wrap(@documents.tags.to_a.join(', '), length:).
79
79
  gsub(/(?<!\A)^/, ' ' * 4)
80
80
  output.puts <<~EOT
@@ -0,0 +1,106 @@
1
+ module OllamaChat::ModelHandling
2
+ # Caches model metadata for one or more Ollama instances.
3
+ #
4
+ # This class provides a simple in-memory cache to store and retrieve
5
+ # model metadata, reducing the need for repeated `ollama show` calls.
6
+ # It supports multiple Ollama instances by using the instance's
7
+ # object ID as part of the cache key.
8
+ class ModelCache
9
+ # Initializes a new model cache.
10
+ def initialize
11
+ @models = Hash.new { |h, k| h[k] = {} }
12
+ @sizes = Hash.new { |h, k| h[k] = {} }
13
+ @registered = Hash.new(false)
14
+ end
15
+
16
+ # Caches model metadata (and optionally size) for a given Ollama
17
+ # instance and model name.
18
+ #
19
+ # Existing metadata is never overwritten (`||=`); only missing
20
+ # entries are filled in. Size, when provided, overwrites unconditionally.
21
+ #
22
+ # @param ollama [Ollama::Client] the Ollama client instance
23
+ # @param name [String, Symbol] the model name
24
+ # @param value [ModelMetadata, nil] the metadata to cache (defaults to nil)
25
+ # @param size [Integer, nil] the model size in bytes (optional)
26
+ # @return [ModelMetadata, nil] the cached metadata
27
+ def cache(ollama, name, value = nil, size: nil)
28
+ name = name.to_s
29
+ @models[ollama.__id__][name] ||= value
30
+ size and @sizes[ollama.__id__][name] = size
31
+ @models[ollama.__id__][name]
32
+ end
33
+
34
+ # Returns the cached `ModelMetadata` for a given model, if it has
35
+ # been fetched. Note: this is a *metadata* predicate, not an
36
+ # existence check. A model can be registered (via `models`) and
37
+ # have a size, yet still return `nil` here until metadata is
38
+ # explicitly cached (e.g. by `model_present?`).
39
+ #
40
+ # @param ollama [Ollama::Client] the Ollama client instance
41
+ # @param name [String, Symbol] the model name
42
+ # @return [ModelMetadata, nil] the metadata if fetched, nil otherwise
43
+ def cached?(ollama, name)
44
+ name = name.to_s
45
+ @models[ollama.__id__][name]
46
+ end
47
+
48
+ # Returns the cached size in bytes for a given model.
49
+ #
50
+ # @param ollama [Ollama::Client] the Ollama client instance
51
+ # @param name [String, Symbol] the model name
52
+ # @return [Integer] the size in bytes, 0 if not cached
53
+ def model_size(ollama, name)
54
+ @sizes[ollama.__id__][name].to_i
55
+ end
56
+
57
+ # Returns a list of cached model names for the given Ollama instance.
58
+ #
59
+ # @param ollama [Ollama::Client] the Ollama client instance
60
+ # @return [Array<String>] the cached model names
61
+ def models(ollama)
62
+ registered?(ollama) or register_all(ollama)
63
+ @models[ollama.__id__].keys
64
+ end
65
+
66
+ # Flushes cached metadata and sizes for one or all Ollama instances.
67
+ #
68
+ # @param ollama [Ollama::Client, Integer, nil] an Ollama client instance,
69
+ # a raw object ID, or `nil` to flush all instances (default: nil)
70
+ # @return [true]
71
+ def flush(ollama = nil)
72
+ if ollama
73
+ ollama_id = ollama.ask_and_send(:to_int) || ollama.__id__
74
+ @models[ollama_id].clear
75
+ @sizes[ollama_id].clear
76
+ @registered[ollama_id] = false
77
+ else
78
+ @models.each_key { |ollama_id| flush(ollama_id) }
79
+ end
80
+ true
81
+ end
82
+
83
+ private
84
+
85
+ # Checks whether model registration has been completed for the instance.
86
+ #
87
+ # @param ollama [Ollama::Client] the Ollama client instance
88
+ # @return [Boolean]
89
+ def registered?(ollama)
90
+ @registered[ollama.__id__]
91
+ end
92
+
93
+ # Registers multiple model names as cached for the given Ollama instance.
94
+ #
95
+ # @param ollama [Ollama::Client] the Ollama client instance
96
+ #
97
+ # @return [ ModelCache ] this cache instance
98
+ def register_all(ollama)
99
+ ollama.tags.models.each do |model|
100
+ cache(ollama, model.name, size: model.size)
101
+ end
102
+ @registered[ollama.__id__] = true
103
+ self
104
+ end
105
+ end
106
+ end
@@ -1,3 +1,5 @@
1
+ require 'ollama_chat/model_handling/model_cache'
2
+
1
3
  # A module that provides functionality for managing Ollama models, including
2
4
  # checking model availability, pulling models from remote servers, and handling
3
5
  # model presence verification.
@@ -416,16 +418,20 @@ module OllamaChat::ModelHandling
416
418
  # @return [ ModelMetadata, NilClass ] if the model is present,
417
419
  # nil otherwise
418
420
  def model_present?(model, ollama:)
421
+ name = model.full? or return
422
+ model_metadata = @model_cache.cached?(ollama, name) and return model_metadata
419
423
  ollama.show(model:) do |md|
420
- return ModelMetadata.new(
421
- name: model,
424
+ model_metadata = ModelMetadata.new(
425
+ name: ,
422
426
  system: md.system,
423
427
  capabilities: md.capabilities,
424
428
  families: md.details.families,
425
429
  thinking: md.thinking,
426
430
  )
427
431
  end
428
- rescue Ollama::Errors::NotFoundError
432
+ model_metadata or return
433
+ @model_cache.cache(ollama, name, model_metadata)
434
+ rescue Ollama::Errors::NotFoundError, Ollama::Errors::BadRequestError
429
435
  nil
430
436
  end
431
437
 
@@ -467,16 +473,15 @@ module OllamaChat::ModelHandling
467
473
  # by creating a formatted string that includes the model name and its size
468
474
  # in a human-readable format with appropriate units.
469
475
  #
470
- # @param model [ Object ] the model object that has name and size attributes
476
+ # @param model [ String ] the model name
477
+ # @param size [ Integer ] the size of the model in bytes
471
478
  #
472
- # @return [ Object ] a result object with an overridden to_s method
473
- # that combines the model name and formatted size
474
- def model_with_size(model, favourited: false)
475
- formatted_size = Term::ANSIColor.bold {
476
- format_bytes(model.size)
477
- }
478
- display = prefix_favourite("#{model.name} #{formatted_size}", favourited)
479
- SearchUI::Wrapper.new(model.name, display:)
479
+ # @return [ SearchUI::Wrapper ] a result object with an overridden to_s
480
+ # method that combines the model name and formatted size
481
+ def model_with_size(model, size, favourited: false)
482
+ formatted_size = Term::ANSIColor.bold { format_bytes(size) }
483
+ display = prefix_favourite("#{model} #{formatted_size}", favourited)
484
+ SearchUI::Wrapper.new(model, display:)
480
485
  end
481
486
 
482
487
  # Ensures the specified model is available locally and synchronizes the
@@ -625,8 +630,13 @@ module OllamaChat::ModelHandling
625
630
  # metadata
626
631
  def all_models
627
632
  favs = all_favourited('model')
628
- ollama.tags.models.sort_by(&:name).
629
- map { |m| model_with_size(m, favourited: favs[m.name]) }
633
+ @model_cache.models(ollama).sort.map { |name|
634
+ model_with_size(
635
+ name,
636
+ @model_cache.model_size(ollama, name),
637
+ favourited: favs[name]
638
+ )
639
+ }
630
640
  end
631
641
 
632
642
  # The choose_model method selects a model from the available list based on
@@ -641,22 +651,24 @@ module OllamaChat::ModelHandling
641
651
  # @param current_model [String] the fallback model if selection fails
642
652
  # @return [String] the selected model name
643
653
  def choose_model(cli_model, current_model)
654
+ if model_present?(cli_model, ollama:)
655
+ return cli_model
656
+ end
644
657
  selector = if cli_model =~ /\A\?+(.*)\z/
645
658
  cli_model = ''
646
659
  Regexp.new($1)
647
660
  end
648
661
  models = all_models
649
662
  selector and models = models.select { _1.value =~ selector }
650
- model =
651
- if models.size == 1
652
- models.first.value
653
- elsif cli_model == ''
654
- choose_entry(
655
- models,
656
- prompt: "Which digital oracle shall we consult? %s"
657
- )&.value || current_model
658
- else
659
- cli_model || current_model
660
- end
663
+ if models.size == 1
664
+ models.first.value
665
+ elsif cli_model == ''
666
+ choose_entry(
667
+ models,
668
+ prompt: "Which digital oracle shall we consult? %s"
669
+ )&.value || current_model
670
+ else
671
+ cli_model || current_model
672
+ end
661
673
  end
662
674
  end
@@ -174,7 +174,7 @@ prompts:
174
174
 
175
175
  Create a title with a length of **less than %{length}** characters for
176
176
  the conversation above. Output only the title and nothing else.
177
- session:
177
+ report:
178
178
  coding_brief: |
179
179
  %{content}
180
180