ollama_chat 0.0.95 → 0.0.96

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +51 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/chat.rb +1 -0
  5. data/lib/ollama_chat/commands.rb +1 -6
  6. data/lib/ollama_chat/database/models/prompt.rb +1 -3
  7. data/lib/ollama_chat/follow_chat.rb +4 -5
  8. data/lib/ollama_chat/message_list.rb +2 -4
  9. data/lib/ollama_chat/ollama_chat_config/default_config.yml +1 -0
  10. data/lib/ollama_chat/parsing.rb +10 -6
  11. data/lib/ollama_chat/personae_management.rb +24 -10
  12. data/lib/ollama_chat/session_management.rb +22 -28
  13. data/lib/ollama_chat/source_fetching.rb +1 -1
  14. data/lib/ollama_chat/token_estimator/crude.rb +25 -0
  15. data/lib/ollama_chat/token_estimator.rb +36 -0
  16. data/lib/ollama_chat/tool_calling.rb +1 -1
  17. data/lib/ollama_chat/tools/compute_bmi.rb +6 -4
  18. data/lib/ollama_chat/tools/execute_grep.rb +11 -1
  19. data/lib/ollama_chat/tools/generate_password.rb +4 -1
  20. data/lib/ollama_chat/tools/get_current_weather.rb +11 -0
  21. data/lib/ollama_chat/tools/get_time.rb +13 -2
  22. data/lib/ollama_chat/tools/open_file_in_editor.rb +3 -3
  23. data/lib/ollama_chat/tools/read_file.rb +3 -0
  24. data/lib/ollama_chat/tools/retrieve_document_snippets.rb +17 -10
  25. data/lib/ollama_chat/tools/roll_dice.rb +52 -11
  26. data/lib/ollama_chat/tools/run_tests.rb +23 -11
  27. data/lib/ollama_chat/tools/write_file.rb +2 -6
  28. data/lib/ollama_chat/utils/fetcher.rb +2 -2
  29. data/lib/ollama_chat/utils/png_metadata_extractor.rb +5 -4
  30. data/lib/ollama_chat/utils.rb +0 -1
  31. data/lib/ollama_chat/version.rb +1 -1
  32. data/lib/ollama_chat.rb +1 -0
  33. data/ollama_chat.gemspec +6 -6
  34. data/spec/ollama_chat/database/models/favourite_spec.rb +7 -20
  35. data/spec/ollama_chat/server_socket_spec.rb +11 -20
  36. data/spec/ollama_chat/state_selectors_spec.rb +4 -9
  37. data/spec/ollama_chat/switches_spec.rb +1 -1
  38. data/spec/ollama_chat/token_estimator_spec.rb +41 -0
  39. data/spec/ollama_chat/tools/compute_bmi_spec.rb +14 -5
  40. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +5 -5
  41. data/spec/ollama_chat/tools/execute_grep_spec.rb +10 -0
  42. data/spec/ollama_chat/tools/generate_image_spec.rb +8 -8
  43. data/spec/ollama_chat/tools/generate_password_spec.rb +13 -0
  44. data/spec/ollama_chat/tools/get_current_weather_spec.rb +4 -0
  45. data/spec/ollama_chat/tools/get_endoflife_spec.rb +3 -3
  46. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -2
  47. data/spec/ollama_chat/tools/get_time_spec.rb +4 -0
  48. data/spec/ollama_chat/tools/get_url_spec.rb +7 -7
  49. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +4 -4
  50. data/spec/ollama_chat/tools/read_file_spec.rb +3 -0
  51. data/spec/ollama_chat/tools/retrieve_document_snippets_spec.rb +36 -1
  52. data/spec/ollama_chat/tools/roll_dice_spec.rb +4 -4
  53. data/spec/ollama_chat/tools/run_tests_spec.rb +5 -5
  54. data/spec/ollama_chat/tools/search_web_spec.rb +3 -3
  55. data/spec/ollama_chat/tools/write_file_spec.rb +2 -0
  56. metadata +9 -5
  57. data/lib/ollama_chat/utils/token_estimator.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5ae8b473c0dfaba1476f7890f8d1e04244db7f19c9dd9d563a845d9a5798a5f
4
- data.tar.gz: 6e0f053ce8c5f9bbe97d5e04d3819fbee2ef9857f6b4c9d6bc4d76fe7e12dffa
3
+ metadata.gz: 54aa30ed02a44f43f0f197247d4e7f5377007802025ddc55632f60965e6d0548
4
+ data.tar.gz: aa6e058dd8135e265fbfbc848620ece0730182ff6c11e4b0e9c87a8b68e97590
5
5
  SHA512:
6
- metadata.gz: 9e514fe65a9e3ccddef894cfe92fdff0e17a1c9ca639994ab54edcd573b219cca6c50774640cd966e8adc4d3232071abacc193c8dd795f9ed6be6460109b62df
7
- data.tar.gz: 681c439ba46ee38cce5201696e40dda4dc6f6d2291c505e420c7fa2d63899ffd0e82068e94c6f8509f170e6843480924c969730c328a3c6a92b00702115de994
6
+ metadata.gz: 96d53286b971b4863f688a8613e9ca3fe3e0ee65b9c5f18b042ea3a910ea8ef8cdb4b2e2b26750c08aae6bcbbdd43371e8f0fd9598f24c494256f58f42ddd8ef
7
+ data.tar.gz: 1d5aa2eadaa9723f6a20f1019f6965249e9d526d38bd6de5d5e86ed1b76ac82e86bf0e59915330f35a2a48f0ce99c6a4109a50c94d38e30965f4d658be768ce3
data/CHANGES.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-07-03 v0.0.96
4
+
5
+ ### New Features
6
+
7
+ - Added reroll capability to the `roll_dice` tool, including a new `reroll`
8
+ configuration option in `default_config.yml` and implementation of a reroll
9
+ loop using `chat.confirm?`.
10
+ - Enhanced the `roll_dice` tool output to include `min` and `max` bounds in the
11
+ JSON response and result range in the summary message.
12
+ - Improved character importing by implementing `import_persona_from_json` in
13
+ `PersonaeManagement`, removing temporary file dependencies.
14
+ - Added the ability to offer new sessions during session selection via
15
+ `offer_new_session: true` in `SessionManagement`.
16
+
17
+ ### Improvements
18
+
19
+ - **Tool Messaging**: Added or enhanced human-readable summary `message` fields
20
+ for several tools to improve user experience:
21
+ - `GetTime`: Now includes time-of-day greetings and formatted weekdays.
22
+ - `GetCurrentWeather`: Now incorporates temperature, conditions, and daily
23
+ forecasts.
24
+ - `ReadFile` & `WriteFile`: Integrated `OllamaChat::TokenEstimator` for
25
+ formatted byte and token counts.
26
+ - `GeneratePassword`: Added summaries covering various alphabet types and
27
+ errors.
28
+ - `RetrieveDocumentSnippets`: Added counts and collection details.
29
+ - `ExecuteGrep`: Added logic to distinguish between found matches and blank
30
+ results.
31
+ - `RunTests`: Added emoji-based result summaries and removed reliance on
32
+ the global `$?` variable.
33
+ - `ComputeBMI`: Enhanced summary messages and simplified calculation logic.
34
+ - **Character Personalization**:
35
+ - Updated `convert_json_character_to_markdown` to replace `{{char}}`
36
+ placeholders with actual names.
37
+ - Enhanced `personalize_character_profile` using case-insensitive regexes
38
+ for `{{user}} and {{char}}`.
39
+ - Refactored `PNGMetadataExtractor.decode_character` to return a tuple of
40
+ the decoded string and parsed JSON data.
41
+ - **Token Estimation**:
42
+ - Relocated `TokenEstimator` to a primary domain module and introduced an
43
+ `Estimate` struct for formatted bytes and tokens.
44
+ - Implemented `OllamaChat::TokenEstimator::Crude` as the estimation engine.
45
+
46
+ ### Refactoring & Maintenance
47
+
48
+ - Updated Ruby syntax across the project to use shorthand hash syntax (e.g., in
49
+ `.where`, object initialization, and method calls).
50
+ - Improved compatibility by replacing the `it` parameter with `_1` in the
51
+ `tools` method within `ToolCalling`.
52
+ - Bumped `context_spook` dependency from **1.5** to **1.6**.
53
+
3
54
  ## 2026-06-29 v0.0.95
4
55
 
5
56
  ### New Features & Enhancements
data/Rakefile CHANGED
@@ -56,7 +56,7 @@ GemHadar do
56
56
  dependency 'bigdecimal', '~> 3.1'
57
57
  dependency 'csv', '~> 3.0'
58
58
  dependency 'const_conf', '~> 0.3'
59
- dependency 'context_spook', '~> 1.5'
59
+ dependency 'context_spook', '~> 1.6'
60
60
  dependency 'infobar', '>= 0.13.1'
61
61
  dependency 'rubyzip', '~> 3.0'
62
62
  dependency 'sequel', '~> 5.0'
@@ -33,6 +33,7 @@ require 'context_spook'
33
33
  class OllamaChat::Chat
34
34
  include Tins::GO
35
35
  include Term::ANSIColor
36
+ include OllamaChat::TokenEstimator
36
37
  include OllamaChat::HTTPHandling
37
38
  include OllamaChat::Commands
38
39
  include OllamaChat::Logging
@@ -724,12 +724,7 @@ module OllamaChat::Commands
724
724
  disable_content_parsing
725
725
  data
726
726
  when 'import'
727
- markdown = convert_json_character_to_markdown(data)
728
- Tempfile.create('character.md') do |tmp|
729
- tmp.puts markdown
730
- tmp.flush
731
- import_persona(Pathname.new(tmp.path))
732
- end
727
+ import_persona_from_json(data)
733
728
  :next
734
729
  end
735
730
  end
@@ -52,9 +52,7 @@ class OllamaChat::Database::Models::Prompt < Sequel::Model(OllamaChat::DB)
52
52
  # database when the underlying prompt is removed.
53
53
  def after_destroy
54
54
  super
55
- OllamaChat::Database::Models::Favourite.
56
- where(context: context, name: name).
57
- destroy
55
+ OllamaChat::Database::Models::Favourite.where(context:, name:).destroy
58
56
  end
59
57
 
60
58
  # Seeds the prompt table from the provided chat configuration.
@@ -165,7 +165,7 @@ class OllamaChat::FollowChat
165
165
  STDOUT.printf(
166
166
  "\n%s Execution of tool %s confirmed.\n\n", symbol, bold { name }
167
167
  )
168
- result = OllamaChat::Tools.registered[name].execute(tool_call, chat: chat)
168
+ result = OllamaChat::Tools.registered[name].execute(tool_call, chat:)
169
169
  if confirmed == :explicit
170
170
  chat.log(:info, "Execution of tool %s was explicitly confirmed." % name)
171
171
  else
@@ -191,14 +191,13 @@ class OllamaChat::FollowChat
191
191
  false
192
192
  end
193
193
 
194
- size_bytes = result.to_s.size
195
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
194
+ es = OllamaChat::TokenEstimator.estimate(result.to_s)
196
195
 
197
196
  tools_used[name] = {
198
197
  message:,
199
198
  warn: ,
200
- size: format_bytes(size_bytes),
201
- tokens: format_tokens(tokens),
199
+ size: es.bytes_formatted,
200
+ tokens: es.tokens_formatted,
202
201
  duration: Tins::Duration.new(Time.now - start).to_s,
203
202
  }
204
203
  end
@@ -380,9 +380,7 @@ class OllamaChat::MessageList
380
380
  def show_system_prompt
381
381
  current_system = system.to_s
382
382
  size_bytes = current_system.size
383
- size = format_bytes(size_bytes)
384
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
385
- tokens_size = format_tokens(tokens)
383
+ es = OllamaChat::TokenEstimator.estimate(size_bytes)
386
384
  system_prompt = @chat.kramdown_ansi_parse(current_system).
387
385
  gsub(/\n+\z/, '').full?
388
386
  if system_prompt.blank?
@@ -398,7 +396,7 @@ class OllamaChat::MessageList
398
396
  #{system_prompt}
399
397
 
400
398
  System prompt name: #{bold{system_name}}
401
- System prompt length: 👾#{size} 🧩#{tokens_size}
399
+ System prompt length: 👾#{es.bytes_formatted} 🧩#{es.tokens_formatted}
402
400
  EOT
403
401
  end
404
402
  self
@@ -402,6 +402,7 @@ tools:
402
402
  timeout_duration: 66
403
403
  roll_dice:
404
404
  default: true
405
+ reroll: false
405
406
  result_display_timeout: 10
406
407
  get_ghr:
407
408
  default: true
@@ -76,9 +76,10 @@ module OllamaChat::Parsing
76
76
  results = []
77
77
 
78
78
  if data = metadata.delete('chara') and
79
- char = OllamaChat::Utils::PNGMetadataExtractor.decode_character(data)
79
+ (char, char_data = OllamaChat::Utils::PNGMetadataExtractor.decode_character(data))
80
80
  then
81
- results << "Character Profile:\n\n#{personalize_character_profile(char)}"
81
+ name = char_data.dig('data', 'name').full? || Pathname.new(source_io.path).basename.sub_ext('')
82
+ results << "Character Profile:\n\n#{personalize_character_profile(char, name:)}"
82
83
  end
83
84
 
84
85
  if data = metadata.delete('parameters') and
@@ -222,13 +223,16 @@ module OllamaChat::Parsing
222
223
  )
223
224
  end
224
225
 
225
- # Personalizes a character profile by replacing the {{user}} placeholder.
226
+ # Personalizes a character profile by replacing placeholders with actual names.
226
227
  #
227
228
  # @param char [String] The raw character JSON string.
229
+ # @param name [String] The name of the character to replace {{char}} with.
228
230
  # @return [String] The personalized character profile.
229
- def personalize_character_profile(char)
230
- name = user_name || 'the user'
231
- char.gsub('{{user}}', name)
231
+ def personalize_character_profile(char, name:)
232
+ my_user_name = user_name || 'the user'
233
+ char = char.gsub(/{{user}}/i, my_user_name)
234
+ name.present? and char = char.gsub(/{{char}}/i, name)
235
+ char
232
236
  end
233
237
 
234
238
  # Regular expression to scan content for url/file references
@@ -351,14 +351,11 @@ module OllamaChat::PersonaeManagement
351
351
  [ pathname, pathname.size ]
352
352
  end.compact.sort_by(&:last).reverse_each do |pathname, size_bytes|
353
353
  persona_name = pathname.basename.sub_ext('').to_s
354
- size_bytes = pathname.size
355
- size = format_bytes(size_bytes)
356
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
357
- tokens_size = format_tokens(tokens)
354
+ es = OllamaChat::TokenEstimator.estimate(size_bytes)
358
355
  is_default = default_persona_name == persona_name
359
356
  display_name = prefix_favourite(is_default ? bold { persona_name } : persona_name, favs[persona_name])
360
357
 
361
- table << [ display_name, size, tokens_size, ]
358
+ table << [ display_name, es.bytes_formatted, es.tokens_formatted, ]
362
359
  end
363
360
 
364
361
  table.align_column 1, :right
@@ -554,24 +551,41 @@ module OllamaChat::PersonaeManagement
554
551
  persona_name
555
552
  end
556
553
 
554
+ # Imports a character persona from JSON data, prompts for a name,
555
+ # and saves the resulting Markdown profile to disk.
556
+ #
557
+ # @param json_data [String] The raw character data in JSON format.
558
+ # @return [String, nil] The name of the created persona if successful,
559
+ # or nil if the process was cancelled or failed.
560
+ def import_persona_from_json(json_data)
561
+ persona_name = determine_valid_new_name_for_persona('to import from JSON/PNG') or return
562
+ markdown = convert_json_character_to_markdown(json_data, persona_name)
563
+ persona_pathname = persona_name_to_pathname(persona_name)
564
+ persona_pathname.write(markdown)
565
+ persona_name
566
+ end
567
+
557
568
  # Transforms raw character data (JSON or YAML) into a high-fidelity,
558
569
  # structured Markdown persona profile using the persona architect prompt and
559
570
  # the current persona template.
560
571
  #
561
572
  # This method leverages the LLM to interpret raw attributes and expand them
562
573
  # into evocative prose, ensuring the final output conforms to the system's
563
- # standard persona structure. It also normalizes placeholder syntax to ensure
564
- # compatibility with the internal persona system.
574
+ # standard persona structure. It also normalizes placeholders: {{user}} is
575
+ # converted to %{user} for runtime personalization, and {{char}} is replaced
576
+ # with the actual character name provided.
565
577
  #
566
578
  # @param character [String] the raw character data in JSON format
579
+ # @param persona_name [String] the name of the character to replace {{char}} with
567
580
  # @return [String] the resulting structured Markdown persona profile
568
- def convert_json_character_to_markdown(character)
569
- generate(
581
+ def convert_json_character_to_markdown(character, persona_name)
582
+ response = generate(
570
583
  prompt: prompt(:persona_architect).to_s % {
571
584
  character:,
572
585
  persona_template: prompt(:persona).to_s
573
586
  }
574
- ).response.gsub('{{user}}', '%{user}')
587
+ ).response
588
+ response.gsub(/{{user}}/i, '%{user}').gsub(/{{char}}/i, persona_name)
575
589
  end
576
590
 
577
591
  # Interactively exports a persona profile to a specified file.
@@ -104,15 +104,12 @@ module OllamaChat::SessionManagement
104
104
  else
105
105
  name
106
106
  end
107
- size_bytes = s.messages.to_s.size
108
- size = format_bytes(size_bytes)
109
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
110
- tokens_size = format_tokens(tokens)
107
+ es = OllamaChat::TokenEstimator.estimate(s.messages.to_s)
111
108
  table << [
112
109
  s.id.to_s,
113
110
  name,
114
- size,
115
- tokens_size,
111
+ es.bytes_formatted,
112
+ es.tokens_formatted,
116
113
  s.messages.to_s.count(?\n),
117
114
  s.age(now:),
118
115
  ]
@@ -130,12 +127,10 @@ module OllamaChat::SessionManagement
130
127
  #
131
128
  # @param output [IO] the output stream to write the information to (default: STDOUT)
132
129
  def show_session(output: STDOUT)
133
- size_bytes = session.messages.to_s.size
134
- messages_size = format_bytes(size_bytes)
135
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
136
- tokens_size = format_tokens(tokens)
130
+ size_bytes = session.messages.to_s.size
131
+ es = OllamaChat::TokenEstimator.estimate(size_bytes)
137
132
  messages_count = session.messages.to_s.count(?\n)
138
- output.puts "#{bold{session.name}} (#{italic{session.id}}), #{messages_size}/#{tokens_size}, #{messages_count} messages"
133
+ output.puts "#{bold{session.name}} (#{italic{session.id}}), #{es.bytes_formatted}/#{es.tokens_formatted}, #{messages_count} messages"
139
134
  end
140
135
 
141
136
  # Interactively prompts the user for a unique session name.
@@ -227,7 +222,7 @@ module OllamaChat::SessionManagement
227
222
  # @return [OllamaChat::Database::Models::Session] the initialized session
228
223
  def setup_session
229
224
  @session = if session_name = @opts[?l]
230
- choose_session(session_name)
225
+ choose_session(session_name, offer_new_session: true)
231
226
  elsif @opts[?n]
232
227
  new_session
233
228
  else
@@ -258,7 +253,8 @@ module OllamaChat::SessionManagement
258
253
  will be deleted, pick a new session to switch to.
259
254
  EOT
260
255
  confirm?(prompt: "\n⏎ Press any key to continue (%s). ", timeout: 3)
261
- if chosen_session = choose_session(??, except_id: current_session_id)
256
+ if chosen_session = choose_session(??, except_id: current_session_id, offer_new_session: true)
257
+ chosen_session.save
262
258
  confirm?(
263
259
  prompt: "🔔 Delete session #{current_session_name.inspect} (#{current_session_id})? (y/n) ",
264
260
  yes: /\Ay/i
@@ -476,22 +472,20 @@ module OllamaChat::SessionManagement
476
472
  elsif selector
477
473
  now = Time.now
478
474
  sessions = session_query.order(Sequel.desc(:updated_at)).map { |session|
479
- duration = session.age(now:)
480
- size_bytes = session.messages.to_s.size
481
- tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
482
- tokens_size = format_tokens(tokens)
483
- count = session.messages.to_s.count(?\n)
484
- locked = if pid = session.locked?
485
- if pid == $$
486
- " 🔓#{pid} "
487
- else
488
- " 🔐#{pid} "
489
- end
490
- else
491
- ' '
492
- end
475
+ duration = session.age(now:)
476
+ es = OllamaChat::TokenEstimator.estimate(session.messages.to_s)
477
+ count = session.messages.to_s.count(?\n)
478
+ locked = if pid = session.locked?
479
+ if pid == $$
480
+ " 🔓#{pid} "
481
+ else
482
+ " 🔐#{pid} "
483
+ end
484
+ else
485
+ ' '
486
+ end
493
487
  display = <<~EOT.strip
494
- #{session.name} 🆔#{session.id}#{locked}📨#{count} 🧩#{tokens_size} ⏳#{duration}
488
+ #{session.name} 🆔#{session.id}#{locked}📨#{count} 🧩#{es.tokens_formatted} ⏳#{duration}
495
489
  EOT
496
490
  SearchUI::Wrapper.new(
497
491
  session.name,
@@ -246,7 +246,7 @@ module OllamaChat::SourceFetching
246
246
  source_io.rewind
247
247
  embed_source(source_io, source, tags:) or return
248
248
  end
249
- prompt(:embed).to_s % { collection: collection }
249
+ prompt(:embed).to_s % { collection: }
250
250
  end
251
251
 
252
252
  private
@@ -0,0 +1,25 @@
1
+ # Provides a "best-effort" estimation of token counts based on the
2
+ # character count or byte size of the input content.
3
+ class OllamaChat::TokenEstimator::Crude
4
+ # Initializes a new crude estimator with the provided source.
5
+ #
6
+ # @param arg [String, Integer] The content to estimate (string or raw byte count).
7
+ # @raise [ArgumentError] if the input is not a string or an integer.
8
+ def initialize(arg)
9
+ if text = arg.ask_and_send(:to_str)
10
+ @bytes = text.size
11
+ elsif bytes = arg.ask_and_send(:to_int)
12
+ @bytes = bytes
13
+ else
14
+ raise ArgumentError, "#{arg.inspect} cannot be used to estimate"
15
+ end
16
+ end
17
+
18
+ # Performs the estimation calculation and returns an Estimate object.
19
+ #
20
+ # @return [OllamaChat::TokenEstimator::Estimate]
21
+ def perform
22
+ tokens = (@bytes.to_f / 3.5).ceil
23
+ OllamaChat::TokenEstimator::Estimate.new(bytes: @bytes, tokens:)
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # Provides tools for estimating token counts across different models and contexts.
2
+ module OllamaChat::TokenEstimator
3
+ end
4
+
5
+ # Requires the core estimation implementations.
6
+ require 'ollama_chat/token_estimator/crude'
7
+
8
+ # A data structure that holds the results of a token estimation,
9
+ # providing convenient formatting methods for both byte size and token count.
10
+ module OllamaChat::TokenEstimator
11
+ # Represents the result of a calculation including raw values
12
+ # and their human-readable formatted strings.
13
+ class Estimate < Struct.new(:bytes, :tokens)
14
+ include OllamaChat::Utils::ValueFormatter
15
+
16
+ # Returns the byte count in a formatted string (e.g., "1.2 KB").
17
+ # @return [String] The formatted byte size.
18
+ def tokens_formatted
19
+ format_tokens(tokens)
20
+ end
21
+
22
+ # Returns the byte count as a formatted string.
23
+ # @return [String] The formatted byte size.
24
+ def bytes_formatted
25
+ format_bytes(bytes)
26
+ end
27
+ end
28
+
29
+ # Estimates token count for a given piece of content.
30
+ #
31
+ # @param text [String, Integer] The content to estimate (string or raw byte count).
32
+ # @return [OllamaChat::TokenEstimator::Estimate] An object containing the results.
33
+ def self.estimate(text)
34
+ OllamaChat::TokenEstimator::Crude.new(text).perform
35
+ end
36
+ end
@@ -55,7 +55,7 @@ module OllamaChat::ToolCalling
55
55
  # @return [ Hash ] a hash containing the registered tools
56
56
  def tools
57
57
  tools_support.off? and return []
58
- enabled_tools.filter_map { OllamaChat::Tools.registered[it]&.to_hash }
58
+ enabled_tools.filter_map { OllamaChat::Tools.registered[_1]&.to_hash }
59
59
  end
60
60
 
61
61
  # The default_enabled_tools method returns an array of tool names that are
@@ -85,13 +85,15 @@ class OllamaChat::Tools::ComputeBMI
85
85
  raise OllamaChat::ToolFunctionArgumentError, 'Height must be greater than zero and in kg/lbs' if height <= 0
86
86
  raise OllamaChat::ToolFunctionArgumentError, 'Weight must be less than 3m and in meter/feet' if height > 3
87
87
 
88
- bmi = ( weight / (height**2) ).round(2)
88
+ bmi = ( weight / height**2 ).round(2)
89
89
  category = calculate_category(bmi)
90
+ message = "This BMI is #{bmi}, which falls into the #{category} category."
90
91
 
91
92
  {
92
- bmi: bmi,
93
- category: category,
94
- units: ,
93
+ bmi:,
94
+ category:,
95
+ units:,
96
+ message:,
95
97
  }.to_json
96
98
  rescue => e
97
99
  { error: e.class, message: e.message }.to_json
@@ -88,7 +88,17 @@ class OllamaChat::Tools::ExecuteGrep
88
88
  context = normalize_number(args.context)
89
89
  cmd = eval_template(config, pattern, path, max_results, ignore_case, before, after, context)
90
90
  result = OllamaChat::Utils::Fetcher.execute(cmd, &:read)
91
- { cmd:, result: }.to_json
91
+ message =
92
+ if result.blank?
93
+ "No matches found for #{args.pattern.inspect} in #{path.inspect}."
94
+ else
95
+ "Found some matches for #{args.pattern.inspect} in #{path.inspect}."
96
+ end
97
+ {
98
+ cmd:,
99
+ result:,
100
+ message:,
101
+ }.to_json
92
102
  rescue => e
93
103
  { error: e.class, message: e.message }.to_json
94
104
  end
@@ -137,13 +137,16 @@ class OllamaChat::Tools::GeneratePassword
137
137
  Tins::Token.new(length:, alphabet:)
138
138
  end
139
139
 
140
+ message = "Successfully generated a #{token.length}-character secure password using the #{alphabet_type} alphabet."
141
+
140
142
  result = {
141
143
  password: token,
142
144
  length: token.length,
143
145
  bits: token.bits,
144
- alphabet_type: alphabet_type,
146
+ alphabet_type: ,
145
147
  uppercase: ,
146
148
  extended: ,
149
+ message: ,
147
150
  generated_at: Time.now.iso8601
148
151
  }
149
152
  if alphabet_type == 'default'
@@ -53,8 +53,19 @@ class OllamaChat::Tools::GetCurrentWeather
53
53
  chat = opts[:chat]
54
54
  config = chat.config
55
55
  units = config.location.units =~ /SI/ ? 'si' : 'us'
56
+
56
57
  data = { current_time: Time.now, units: } |
57
58
  JSON(get_weather_data(chat, config, units)).deep_symbolize_keys
59
+
60
+ temp = data.dig(:currently, :temperature)
61
+ curr_sum = data.dig(:currently, :summary)
62
+ daily_sum = data.dig(:daily, :summary)
63
+ unit_sym = units == 'si' ? '°C' : '°F'
64
+
65
+ message = "Currently #{temp}#{unit_sym} and #{curr_sum}. Today's forecast: #{daily_sum}"
66
+
67
+ data[:message] = message
68
+
58
69
  data.to_json
59
70
  rescue => e
60
71
  {
@@ -54,8 +54,19 @@ class OllamaChat::Tools::GetTime
54
54
  # execute(tool_call, config:)
55
55
  # # => {"time":"2026-02-09T14:32:00+01:00","weekday":"Monday"}
56
56
  def execute(_tool_call, **_opts)
57
- now = Time.now
58
- { time: now.iso8601, weekday: now.strftime('%A') }.to_json
57
+ now = Time.now
58
+ hour = now.hour
59
+
60
+ greeting = case hour
61
+ when 5..11 then 'Good morning'
62
+ when 12..17 then 'Good afternoon'
63
+ when 18..23 then 'Good evening'
64
+ else 'Good night'
65
+ end
66
+
67
+ message = "#{greeting}! It is currently #{now.strftime('%H:%M')} on #{now.strftime('%A')}."
68
+
69
+ { time: now.iso8601, weekday: now.strftime('%A'), message: }.to_json
59
70
  end
60
71
 
61
72
  self
@@ -82,10 +82,10 @@ class OllamaChat::Tools::OpenFileInEditor
82
82
 
83
83
  {
84
84
  success: true,
85
- message: ,
85
+ message: ,
86
86
  path: file_path,
87
- start_line: start_line,
88
- end_line: end_line,
87
+ start_line: ,
88
+ end_line: ,
89
89
  }.to_json
90
90
  rescue => e
91
91
  {
@@ -73,12 +73,15 @@ class OllamaChat::Tools::ReadFile
73
73
 
74
74
  path = assert_valid_path(args.path, config.tools.functions.read_file.allowed?, check: :file)
75
75
  content = extract_range(path.read, start_line, end_line)
76
+ es = OllamaChat::TokenEstimator.estimate(content)
77
+ message = "Read #{es.bytes_formatted} (#{es.tokens_formatted}) from #{path.to_s.inspect}."
76
78
 
77
79
  {
78
80
  path:,
79
81
  content:,
80
82
  start_line:,
81
83
  end_line:,
84
+ message:,
82
85
  }.to_json
83
86
  rescue => e
84
87
  {
@@ -123,15 +123,22 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
123
123
  records = rerank_records(chat, query, records)
124
124
  end
125
125
 
126
- message = records.map { |record|
127
- link = if record.source =~ %r(\Ahttps?://)
128
- record.source
129
- else
130
- 'file://%s' % File.expand_path(record.source)
131
- end
132
- link && record.tags.any? or next
133
- [ link, ?# + record.tags.first ]
134
- }.flat_map { |l, t| chat.hyperlink(l, t) }.join(' ')
126
+ collection_name = chat.documents.collection
127
+ message =
128
+ if records.any?
129
+ "Retrieved #{records.size} relevant snippets from collection #{collection_name.inspect} for query #{query.inspect}. See snippets below:\n\n" +
130
+ records.map { |record|
131
+ link = if record.source =~ %r(\Ahttps?://)
132
+ record.source
133
+ else
134
+ 'file://%s' % File.expand_path(record.source)
135
+ end
136
+ link && record.tags.any? or next
137
+ [ link, ?# + record.tags.first ]
138
+ }.flat_map { |l, t| chat.hyperlink(l, t) }.join(' ')
139
+ else
140
+ "No relevant snippets found for query #{query.inspect} in collection #{collection_name.inspect}."
141
+ end
135
142
 
136
143
  {
137
144
  prompt: 'Consider these snippets generated from retrieval when formulating your response!',
@@ -208,7 +215,7 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
208
215
  prompt: chat.config.embedding.model.prompt?,
209
216
  text_size: ,
210
217
  text_count: ,
211
- min_similarity: min_similarity
218
+ min_similarity:
212
219
  )
213
220
  end
214
221