ollama_chat 0.0.134 → 0.0.136

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +121 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/chat.rb +14 -2
  5. data/lib/ollama_chat/commands.rb +14 -11
  6. data/lib/ollama_chat/database/models/session.rb +2 -2
  7. data/lib/ollama_chat/database/{session_locking.rb → row_locking.rb} +11 -16
  8. data/lib/ollama_chat/database.rb +1 -1
  9. data/lib/ollama_chat/follow_chat.rb +1 -135
  10. data/lib/ollama_chat/information.rb +59 -1
  11. data/lib/ollama_chat/model_handling/model_cache.rb +106 -0
  12. data/lib/ollama_chat/model_handling.rb +37 -25
  13. data/lib/ollama_chat/oc.rb +8 -0
  14. data/lib/ollama_chat/ollama_chat_config/default_config.yml +2 -1
  15. data/lib/ollama_chat/ollama_chat_config/fastfetch-profile.jsonc +13 -0
  16. data/lib/ollama_chat/personae_management.rb +20 -2
  17. data/lib/ollama_chat/prompt_management.rb +1 -1
  18. data/lib/ollama_chat/rag_handling.rb +2 -2
  19. data/lib/ollama_chat/session_management.rb +33 -26
  20. data/lib/ollama_chat/source_fetching.rb +1 -1
  21. data/lib/ollama_chat/tool_call_processor.rb +176 -0
  22. data/lib/ollama_chat/tool_calling.rb +0 -16
  23. data/lib/ollama_chat/tools/execute_shell.rb +72 -32
  24. data/lib/ollama_chat/version.rb +7 -2
  25. data/lib/ollama_chat.rb +1 -0
  26. data/ollama_chat.gemspec +7 -7
  27. data/spec/ollama_chat/commands_spec.rb +10 -2
  28. data/spec/ollama_chat/follow_chat_spec.rb +2 -1
  29. data/spec/ollama_chat/information_spec.rb +1 -1
  30. data/spec/ollama_chat/kramdown_ansi_spec.rb +1 -1
  31. data/spec/ollama_chat/model_handling/model_cache_spec.rb +181 -0
  32. data/spec/ollama_chat/personae_management_spec.rb +75 -0
  33. data/spec/ollama_chat/session_management_spec.rb +31 -6
  34. data/spec/ollama_chat/tool_call_processor_spec.rb +51 -0
  35. data/spec/ollama_chat/tool_calling_spec.rb +0 -26
  36. data/spec/ollama_chat/tools/execute_shell_spec.rb +68 -0
  37. metadata +18 -7
@@ -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
@@ -89,6 +89,14 @@ module OC
89
89
  end
90
90
  end
91
91
 
92
+ FASTFETCH = set do
93
+ description 'Path to fastfetch executable for system information'
94
+
95
+ default do
96
+ `which 2>/dev/null fastfetch`.full?(:chomp)
97
+ end
98
+ end
99
+
92
100
  KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES = set do
93
101
  description 'Styles to use for kramdown-ansi markdown'
94
102
 
@@ -56,6 +56,7 @@ prompts:
56
56
  - Location: %{location}
57
57
  - Current directory is "%{current_directory}"
58
58
  - Client is %{client}.
59
+ - Machine: %{machine}
59
60
  dynamic_runtime_info: |
60
61
  There is usually no reason to mention this information to the user unless
61
62
  asked about it.
@@ -174,7 +175,7 @@ prompts:
174
175
 
175
176
  Create a title with a length of **less than %{length}** characters for
176
177
  the conversation above. Output only the title and nothing else.
177
- session:
178
+ report:
178
179
  coding_brief: |
179
180
  %{content}
180
181
 
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
3
+ "display": {
4
+ "showErrors": false,
5
+ "noBuffer": true
6
+ },
7
+ "logo": null,
8
+ "modules": [
9
+ "os", "kernel", "cpu", "gpu", "de", "wm",
10
+ "terminal", "memory", "editor",
11
+ "packages", "chassis", "board", "bios"
12
+ ]
13
+ }
@@ -95,15 +95,33 @@ module OllamaChat::PersonaeManagement
95
95
  def set_default_persona_name(persona_name)
96
96
  if persona_name.present? && persona_name != :none
97
97
  @default_persona_name = Pathname.new(persona_name).basename.sub_ext('').to_path
98
- @session.update(default_persona_name:)
99
98
  else
100
- @session.update(default_persona_name: nil)
101
99
  @default_persona_name = nil
102
100
  end
101
+ @session.update(default_persona_name:)
102
+ sync_trigger
103
103
  messages.set_system_prompt(session&.current_system_prompt.full?)
104
104
  default_persona_name
105
105
  end
106
106
 
107
+ # Synchronizes the session's trigger collection to match the current
108
+ # default persona.
109
+ #
110
+ # When a persona is set, the trigger is re-keyed (or seeded) to
111
+ # `memory-<persona>`, preserving any existing trigger configuration.
112
+ # When no persona is active, the trigger is set to nil.
113
+ def sync_trigger
114
+ unless @default_persona_name
115
+ session.update(trigger: nil) unless session.trigger.nil?
116
+ return
117
+ end
118
+ collection = create_memory_collection(@default_persona_name)
119
+ return if session.trigger&.keys&.first == collection
120
+ cfg = session.trigger&.values&.first or
121
+ cfg = chat.config.trigger.to_h.stringify_keys_recursive
122
+ session.update(trigger: { collection => cfg })
123
+ end
124
+
107
125
  # Interactively selects a persona and sets it as the default for the session.
108
126
  #
109
127
  # This method prompts the user to choose a persona from the available list
@@ -381,7 +381,7 @@ module OllamaChat::PromptManagement
381
381
  start = '%s %s' % [ default, bold { p.name } ]
382
382
  start = prefix_favourite(start, favs[p.name])
383
383
  content = p.to_s.inspect[1..-2]
384
- content = Kramdown::ANSI::Width.truncate(
384
+ content = truncate(
385
385
  content, length: 0.9 * (Tins::Terminal.columns - start.size)
386
386
  )
387
387
  feedback '%s %s' % [ start, italic { content } ]
@@ -336,7 +336,7 @@ module OllamaChat::RAGHandling
336
336
  output.puts kramdown_ansi_parse(<<~EOT)
337
337
  **#{record.source}**
338
338
  ```
339
- #{Kramdown::ANSI::Width.wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
339
+ #{wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
340
340
  ```
341
341
  ---
342
342
  EOT
@@ -406,7 +406,7 @@ module OllamaChat::RAGHandling
406
406
  output.puts kramdown_ansi_parse(<<~EOT)
407
407
  **[#{record.tags.join(', ')}]**
408
408
  ```
409
- #{Kramdown::ANSI::Width.wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
409
+ #{wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
410
410
  ```
411
411
  ---
412
412
  EOT
@@ -110,7 +110,7 @@ module OllamaChat::SessionManagement
110
110
  table.headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |header| bold { header } }
111
111
  now = Time.now
112
112
  models::Session.order(Sequel.desc(:updated_at)).each do |s|
113
- name = Kramdown::ANSI::Width.truncate(s.name, length: 32)
113
+ name = truncate(s.name, length: 32)
114
114
  name = session.id == s.id ? bold { name } : name
115
115
  name = if pid = s.locked?
116
116
  if pid == $$
@@ -197,8 +197,15 @@ module OllamaChat::SessionManagement
197
197
  previous_session_id = @session.id
198
198
  @session = new_session(name:)
199
199
  set_previous_session_on_change(previous_session_id)
200
- session.lock?(chat: self) or raise OllamaChat::OllamaChatError,
201
- "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
200
+ unless session.lock?
201
+ feedback(
202
+ "session #{session.id} locked by running process #{session.locked?}",
203
+ type: :warn
204
+ )
205
+ raise OllamaChat::OllamaChatError,
206
+ "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
207
+ end
208
+ @default_persona_name = session.default_persona_name
202
209
  session_apply
203
210
  messages.clear
204
211
  session.current_model.full? {
@@ -255,10 +262,15 @@ module OllamaChat::SessionManagement
255
262
  preferred_session
256
263
  end
257
264
  session or abort "No session named #{bold{session_name.inspect}} found."
258
- if session.lock?(chat: self)
265
+ if session.lock?
259
266
  messages.read_conversation_jsonl(session.messages.to_s)
267
+ @default_persona_name = session.default_persona_name || nil
260
268
  session_apply
261
269
  else
270
+ feedback(
271
+ "session #{session.id} locked by running process #{session.locked?}",
272
+ type: :warn
273
+ )
262
274
  raise OllamaChat::OllamaChatError,
263
275
  "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
264
276
  end
@@ -279,13 +291,7 @@ module OllamaChat::SessionManagement
279
291
  # @return [OllamaChat::Database::Models::Session] the applied session
280
292
  # instance.
281
293
  def session_apply
282
- initial_persona_name.full? do |persona_name|
283
- collection = create_memory_collection(persona_name)
284
- session.trigger.present? or
285
- session.trigger = {
286
- collection => chat.config.trigger.to_h.stringify_keys_recursive
287
- }
288
- end
294
+ sync_trigger
289
295
  session.update(working_directory: Dir.pwd)
290
296
  init_history
291
297
  repair_group_uuids
@@ -442,7 +448,7 @@ module OllamaChat::SessionManagement
442
448
 
443
449
  # Generates a report document for the current session's conversation.
444
450
  #
445
- # Uses a user-selected prompt template from the 'session' context
451
+ # Uses a user-selected prompt template from the 'report' context
446
452
  # (e.g. coding_brief, roleplay_story) to produce a single cohesive
447
453
  # document describing the session. Uses `compacted_messages` (summary +
448
454
  # recent tail) rather than `each_message` so the interpolated content
@@ -459,10 +465,10 @@ module OllamaChat::SessionManagement
459
465
  content.empty? and return
460
466
 
461
467
  template = if name
462
- prompt(name, context: 'session')
468
+ prompt(name, context: 'report')
463
469
  else
464
470
  choose_prompt(
465
- context: 'session',
471
+ context: 'report',
466
472
  prompt: 'Which report template? %s'
467
473
  )
468
474
  end
@@ -601,7 +607,7 @@ module OllamaChat::SessionManagement
601
607
  name = name.
602
608
  gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
603
609
  gsub(/\s+/, ' ')
604
- Kramdown::ANSI::Width.truncate(name, length:)
610
+ truncate(name, length:)
605
611
  end
606
612
  end
607
613
  end
@@ -649,7 +655,7 @@ module OllamaChat::SessionManagement
649
655
  session_close
650
656
  previous_session = session
651
657
  @session = chosen_session
652
- if session.lock?(chat: self)
658
+ if session.lock?
653
659
  messages.read_conversation_jsonl(session.messages.to_s)
654
660
  if current_collection = session.current_collection.full? and
655
661
  database_collection?(current_collection)
@@ -666,6 +672,10 @@ module OllamaChat::SessionManagement
666
672
  info_session
667
673
  break
668
674
  else
675
+ feedback(
676
+ "session #{session.id} locked by running process #{session.locked?}",
677
+ type: :warn
678
+ )
669
679
  @session = previous_session
670
680
  @session.lock
671
681
  name = ??
@@ -737,16 +747,13 @@ module OllamaChat::SessionManagement
737
747
  else
738
748
  ' '
739
749
  end
740
- display = <<~EOT.strip
741
- #{session.name} 🆔#{session.id}#{locked}📨#{count} 🧩#{es.tokens_formatted} ⏳#{duration}
742
- EOT
743
- SearchUI::Wrapper.new(
744
- session.name,
745
- display:
746
- )
750
+ suffix = " 🆔#{session.id}#{locked}📨#{count} 🧩#{es.tokens_formatted} ⏳#{duration}"
751
+ display_session_name = truncate(session.name, length: (0.8 * (width - suffix.length)).floor.clamp(1..))
752
+ display = "#{display_session_name}#{suffix}"
753
+ SearchUI::Wrapper.new([ session.id, session.name ] * ?-, display:)
747
754
  }
748
755
  selector and sessions = sessions.select { _1 =~ selector }
749
- session_name =
756
+ session_selected =
750
757
  if sessions.size == 1
751
758
  sessions.first.value
752
759
  else
@@ -766,8 +773,8 @@ module OllamaChat::SessionManagement
766
773
  end
767
774
  value
768
775
  end
769
- if session_name
770
- session_query.first(name: session_name)
776
+ if session_selected
777
+ session_query.first(id: session_selected.split(?-, 2).first)
771
778
  end
772
779
  end
773
780
  end
@@ -244,7 +244,7 @@ module OllamaChat::SourceFetching
244
244
  command = false
245
245
  inputs = inputs.map { |chunk| "Source: #{source}\n#{chunk}" }
246
246
  if source.start_with?(?!)
247
- source = Kramdown::ANSI::Width.truncate(
247
+ source = truncate(
248
248
  source[1..-1].gsub(/\W+/, ?_),
249
249
  length: 10
250
250
  )
@@ -0,0 +1,176 @@
1
+ # Processes tool calls from Ollama responses: executes registered tools,
2
+ # collects results, and provides a drain interface for the chat loop to
3
+ # consume them as follow-up messages.
4
+ #
5
+ # This class encapsulates the full tool-call lifecycle that was previously
6
+ # split between {OllamaChat::FollowChat#handle_tool_calls} (execution)
7
+ # and {OllamaChat::ToolCalling#handle_tool_call_results?} (consumption).
8
+ class OllamaChat::ToolCallProcessor
9
+ include Ollama::Handlers::Concern
10
+ include Term::ANSIColor
11
+ include OllamaChat::Feedback
12
+
13
+ # @return [OllamaChat::Chat] the chat context
14
+ attr_reader :chat
15
+
16
+ # The accumulated tool results, keyed by tool name.
17
+ #
18
+ # @return [Hash{String => Array<String>}]
19
+ attr_reader :results
20
+
21
+ # @param chat [OllamaChat::Chat] the chat context
22
+ # @param output [IO] the output stream for feedback (default: STDOUT)
23
+ def initialize(chat:, output: STDOUT)
24
+ super(output:)
25
+ @chat = chat
26
+ @results = Hash.new { |h, name| h[name] = [] }
27
+ end
28
+
29
+ # Processes tool calls from a response, executing each tool and
30
+ # storing the result in the internal results hash.
31
+ #
32
+ # @param response [Ollama::Response] the parsed response
33
+ def handle(response)
34
+ return unless response.message.ask_and_send(:tool_calls)
35
+
36
+ tools_used = {}
37
+
38
+ response.message.tool_calls.each do |tool_call|
39
+ name = tool_call.function.name
40
+ if %r(.*[/:](?<new_name>.+)) =~ name
41
+ msg = "Received namespaced tool call for #{name}, correcting to #{new_name}"
42
+ chat.log(:warn, msg, data: { tool: name, new_name: })
43
+ name = new_name
44
+ end
45
+ unless chat.tool_configured?(name)
46
+ msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name
47
+ @results[name] << msg
48
+ chat.log(:error, msg, data: { tool: name, reason: :unconfigured })
49
+ next
50
+ end
51
+ unless chat.tool_registered?(name)
52
+ msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name
53
+ @results[name] << msg
54
+ chat.log(:error, msg, data: { tool: name, reason: :unregistered })
55
+ next
56
+ end
57
+ unless chat.tool_enabled?(name)
58
+ msg = "Error: Disabled tool named %s ignored => Skip.\n" % name
59
+ @results[name] << msg
60
+ chat.log(:error, msg, data: { tool: name, reason: :disabled })
61
+ next
62
+ end
63
+ feedback(?\n, output:)
64
+ confirmed = :implicit
65
+ resolve = nil
66
+ function = JSON.pretty_generate(tool_call.function)
67
+ chat.log(:info, "Tool call received", data: { tool: name, function: })
68
+ if chat.tool_function(name).require_confirmation?
69
+ feedback(
70
+ ("I want to execute tool %s\n%s\n" % [ bold { name }, italic { function }, ]),
71
+ type: :alarm
72
+ )
73
+ prompt = '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
74
+ answer = chat.confirm?(prompt:)&.to_s&.downcase
75
+ resolve = 'You **MUST** ask the user for instructions on how to proceed!!!'
76
+ case answer
77
+ when 'y'
78
+ confirmed = :explicit
79
+ when 'n'
80
+ confirmed = :denied
81
+ else
82
+ confirmed = :denied
83
+ instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
84
+ resolve = instr.full? || resolve
85
+ end
86
+ else
87
+ feedback(
88
+ "Executing tool %s\n%s" % [ bold { name }, italic { function }, ]
89
+ )
90
+ end
91
+ start = Time.now
92
+ result = nil
93
+ case confirmed
94
+ when :denied
95
+ result = JSON(message: 'User denied confirmation!', resolve:)
96
+ feedback(?\n)
97
+ feedback(
98
+ ("Execution of tool %s denied by user.\n\n" % [ bold { name } ]),
99
+ type: :denied
100
+ )
101
+ chat.log(:warn, "Tool execution denied", data: { tool: name, reason: :denied })
102
+ else
103
+ symbol = confirmed == :implicit ? '☑️ ' : '✅'
104
+ feedback(
105
+ ("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ])
106
+ )
107
+ result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call)
108
+ chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
109
+ end
110
+
111
+ @results[name] << result
112
+ data = nil
113
+ message = begin
114
+ data = JSON.parse(result)
115
+ chat.log(:info, "Tool result", data: { result: data })
116
+ data.to_hash['message'] if data.respond_to?(:to_hash)
117
+ rescue
118
+ chat.log(:info, "Tool result", data: { result: })
119
+ nil
120
+ end
121
+ warn =
122
+ begin
123
+ !!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false
124
+ rescue
125
+ false
126
+ end
127
+
128
+ es = OllamaChat::TokenEstimator.estimate(result.to_s)
129
+
130
+ tools_used[name] = {
131
+ message:,
132
+ warn: ,
133
+ size: es.bytes_formatted,
134
+ tokens: es.tokens_formatted,
135
+ duration: Tins::Duration.new(Time.now - start).to_s,
136
+ }
137
+ end
138
+
139
+ if tools_used.full?
140
+ infobar.reset
141
+ tools_used.each do |name, info|
142
+ feedback_message =
143
+ if info[:message]
144
+ "\n%s %s\n\n" % [ info[:warn] ? '⚠️' : '💡', info[:message] ]
145
+ end
146
+ feedback(<<~EOT.strip)
147
+ 🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}).
148
+ #{feedback_message}
149
+ EOT
150
+ feedback(?\n)
151
+ timeout = chat.tool_function(name).result_display_timeout?
152
+ chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Drains all accumulated results, yielding each entry to the block,
158
+ # then clears the internal storage.
159
+ #
160
+ # @yield [index, tool_name, content] each result entry
161
+ # @return [true, false] whether any results were present
162
+ def drain(&block)
163
+ @results.each do |tool_name, results|
164
+ results.each_with_index { |content, index|
165
+ block.(index, tool_name, content)
166
+ }
167
+ end.present?
168
+ ensure
169
+ @results.clear
170
+ end
171
+
172
+ # Clears all accumulated results without yielding them.
173
+ def clear!
174
+ @results.clear
175
+ end
176
+ end
@@ -94,11 +94,6 @@ module OllamaChat::ToolCalling
94
94
  Array(config.tools&.functions&.attribute_names&.map(&:to_s)).sort
95
95
  end
96
96
 
97
- # The tool_call_results reader returns the tools' results for the
98
- # chat session if any.
99
- #
100
- # @return [ Hash ] a hash containing the registered tool results
101
- attr_reader :tool_call_results
102
97
 
103
98
  # The list_tools method displays the sorted list of enabled tools.
104
99
  #
@@ -183,15 +178,4 @@ module OllamaChat::ToolCalling
183
178
  end
184
179
  end
185
180
 
186
- private
187
-
188
- def handle_tool_call_results?(&block)
189
- @tool_call_results.each do |tool_name, results|
190
- results.each_with_index { |content, index|
191
- block.(index, tool_name, content)
192
- }
193
- end.present?
194
- ensure
195
- @tool_call_results.clear
196
- end
197
181
  end