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 +4 -4
- data/CHANGES.md +76 -0
- data/Gemfile +0 -2
- data/lib/ollama_chat/chat.rb +14 -2
- data/lib/ollama_chat/commands.rb +8 -5
- data/lib/ollama_chat/database/models/session.rb +2 -2
- data/lib/ollama_chat/database/{session_locking.rb → row_locking.rb} +11 -16
- data/lib/ollama_chat/database.rb +1 -1
- data/lib/ollama_chat/follow_chat.rb +1 -135
- data/lib/ollama_chat/information.rb +1 -1
- data/lib/ollama_chat/model_handling/model_cache.rb +106 -0
- data/lib/ollama_chat/model_handling.rb +37 -25
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +1 -1
- data/lib/ollama_chat/personae_management.rb +20 -2
- data/lib/ollama_chat/prompt_management.rb +1 -1
- data/lib/ollama_chat/rag_handling.rb +2 -2
- data/lib/ollama_chat/session_management.rb +33 -26
- data/lib/ollama_chat/source_fetching.rb +1 -1
- data/lib/ollama_chat/tool_call_processor.rb +176 -0
- data/lib/ollama_chat/tool_calling.rb +0 -16
- data/lib/ollama_chat/version.rb +7 -2
- data/lib/ollama_chat.rb +1 -0
- data/ollama_chat.gemspec +6 -6
- data/spec/ollama_chat/commands_spec.rb +8 -0
- data/spec/ollama_chat/follow_chat_spec.rb +2 -1
- data/spec/ollama_chat/information_spec.rb +1 -1
- data/spec/ollama_chat/model_handling/model_cache_spec.rb +181 -0
- data/spec/ollama_chat/personae_management_spec.rb +75 -0
- data/spec/ollama_chat/session_management_spec.rb +31 -6
- data/spec/ollama_chat/tool_call_processor_spec.rb +51 -0
- data/spec/ollama_chat/tool_calling_spec.rb +0 -26
- metadata +15 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7f6ef35b1ea750d80dfbfd13c36ac48abeb22ecbddf53abdb50974108eea0d5
|
|
4
|
+
data.tar.gz: 3cde04bc79e939681722cc75277f8fbc59e3306848bc6f6afbca28c5ed408724
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lib/ollama_chat/chat.rb
CHANGED
|
@@ -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
|
-
@
|
|
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 =
|
|
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,
|
data/lib/ollama_chat/commands.rb
CHANGED
|
@@ -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
|
-
|
|
569
|
-
|
|
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::
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
10
|
-
#
|
|
11
|
-
module OllamaChat::Database::
|
|
12
|
-
# Checks if the
|
|
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
|
|
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?
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
41
|
+
# Releases the lock on the row.
|
|
47
42
|
#
|
|
48
43
|
# @return [Boolean] true if the update was successful.
|
|
49
44
|
def unlock
|
data/lib/ollama_chat/database.rb
CHANGED
|
@@ -89,147 +89,13 @@ class OllamaChat::FollowChat
|
|
|
89
89
|
|
|
90
90
|
output_eval_stats(response)
|
|
91
91
|
|
|
92
|
-
|
|
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 =
|
|
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
|
-
|
|
421
|
-
name:
|
|
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
|
-
|
|
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 [
|
|
476
|
+
# @param model [ String ] the model name
|
|
477
|
+
# @param size [ Integer ] the size of the model in bytes
|
|
471
478
|
#
|
|
472
|
-
# @return [
|
|
473
|
-
#
|
|
474
|
-
def model_with_size(model, favourited: false)
|
|
475
|
-
formatted_size = Term::ANSIColor.bold {
|
|
476
|
-
|
|
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
|
-
|
|
629
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|