ollama_chat 0.0.95 → 0.0.97

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +106 -0
  3. data/README.md +115 -2
  4. data/Rakefile +1 -1
  5. data/lib/ollama_chat/chat.rb +34 -17
  6. data/lib/ollama_chat/commands.rb +27 -15
  7. data/lib/ollama_chat/database/migrations/005_add_context_format_to_sessions.rb +5 -0
  8. data/lib/ollama_chat/database/models/model_options.rb +1 -0
  9. data/lib/ollama_chat/database/models/prompt.rb +1 -3
  10. data/lib/ollama_chat/database/models/session.rb +18 -0
  11. data/lib/ollama_chat/follow_chat.rb +4 -5
  12. data/lib/ollama_chat/information.rb +2 -1
  13. data/lib/ollama_chat/input_content.rb +1 -1
  14. data/lib/ollama_chat/message_list.rb +2 -4
  15. data/lib/ollama_chat/ollama_chat_config/default_config.yml +2 -1
  16. data/lib/ollama_chat/parsing.rb +10 -6
  17. data/lib/ollama_chat/personae_management.rb +25 -11
  18. data/lib/ollama_chat/prompt_management.rb +25 -11
  19. data/lib/ollama_chat/session_management.rb +24 -30
  20. data/lib/ollama_chat/source_fetching.rb +1 -1
  21. data/lib/ollama_chat/state_selectors.rb +14 -0
  22. data/lib/ollama_chat/system_prompt_management.rb +21 -0
  23. data/lib/ollama_chat/think_control.rb +0 -1
  24. data/lib/ollama_chat/token_estimator/crude.rb +25 -0
  25. data/lib/ollama_chat/token_estimator.rb +36 -0
  26. data/lib/ollama_chat/tool_calling.rb +1 -1
  27. data/lib/ollama_chat/tools/compute_bmi.rb +6 -4
  28. data/lib/ollama_chat/tools/execute_grep.rb +11 -1
  29. data/lib/ollama_chat/tools/file_context.rb +3 -2
  30. data/lib/ollama_chat/tools/generate_password.rb +4 -1
  31. data/lib/ollama_chat/tools/get_current_weather.rb +11 -0
  32. data/lib/ollama_chat/tools/get_time.rb +13 -2
  33. data/lib/ollama_chat/tools/open_file_in_editor.rb +3 -3
  34. data/lib/ollama_chat/tools/read_file.rb +3 -0
  35. data/lib/ollama_chat/tools/retrieve_document_snippets.rb +18 -11
  36. data/lib/ollama_chat/tools/roll_dice.rb +52 -11
  37. data/lib/ollama_chat/tools/run_tests.rb +23 -11
  38. data/lib/ollama_chat/tools/write_file.rb +2 -6
  39. data/lib/ollama_chat/utils/fetcher.rb +2 -2
  40. data/lib/ollama_chat/utils/png_metadata_extractor.rb +5 -4
  41. data/lib/ollama_chat/utils.rb +0 -1
  42. data/lib/ollama_chat/version.rb +1 -1
  43. data/lib/ollama_chat.rb +1 -0
  44. data/ollama_chat.gemspec +6 -6
  45. data/spec/ollama_chat/database/models/favourite_spec.rb +7 -20
  46. data/spec/ollama_chat/server_socket_spec.rb +11 -20
  47. data/spec/ollama_chat/source_fetching_spec.rb +1 -1
  48. data/spec/ollama_chat/state_selectors_spec.rb +4 -9
  49. data/spec/ollama_chat/switches_spec.rb +1 -1
  50. data/spec/ollama_chat/token_estimator_spec.rb +41 -0
  51. data/spec/ollama_chat/tools/compute_bmi_spec.rb +14 -5
  52. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +5 -5
  53. data/spec/ollama_chat/tools/execute_grep_spec.rb +10 -0
  54. data/spec/ollama_chat/tools/generate_image_spec.rb +8 -8
  55. data/spec/ollama_chat/tools/generate_password_spec.rb +13 -0
  56. data/spec/ollama_chat/tools/get_current_weather_spec.rb +4 -0
  57. data/spec/ollama_chat/tools/get_endoflife_spec.rb +3 -3
  58. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -2
  59. data/spec/ollama_chat/tools/get_time_spec.rb +4 -0
  60. data/spec/ollama_chat/tools/get_url_spec.rb +7 -7
  61. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +4 -4
  62. data/spec/ollama_chat/tools/read_file_spec.rb +3 -0
  63. data/spec/ollama_chat/tools/retrieve_document_snippets_spec.rb +38 -5
  64. data/spec/ollama_chat/tools/roll_dice_spec.rb +4 -4
  65. data/spec/ollama_chat/tools/run_tests_spec.rb +5 -5
  66. data/spec/ollama_chat/tools/search_web_spec.rb +3 -3
  67. data/spec/ollama_chat/tools/write_file_spec.rb +2 -0
  68. metadata +11 -5
  69. data/lib/ollama_chat/utils/token_estimator.rb +0 -15
@@ -262,13 +262,14 @@ module OllamaChat::PersonaeManagement
262
262
  # Prompts the user to select a persona, copies its filesystem path to the
263
263
  # clipboard, and sets it as the prefill prompt for the next interaction.
264
264
  #
265
+ # @param [Boolean] no_prefill Whether to skip updating the prefill prompt.
265
266
  # @return [String, nil] the filesystem path of the selected persona,
266
267
  # or nil if the selection was cancelled.
267
- def select_persona_path
268
+ def select_persona_path(no_prefill: false)
268
269
  persona = choose_persona(prompt: "Which persona's path do you need? %s") or return
269
270
  path = persona_name_to_pathname(persona).to_s
270
271
  perform_copy_to_clipboard(text: path, edit: false)
271
- @prefill_prompt = path
272
+ no_prefill or @prefill_prompt = path
272
273
  path
273
274
  end
274
275
 
@@ -351,14 +352,11 @@ module OllamaChat::PersonaeManagement
351
352
  [ pathname, pathname.size ]
352
353
  end.compact.sort_by(&:last).reverse_each do |pathname, size_bytes|
353
354
  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)
355
+ es = OllamaChat::TokenEstimator.estimate(size_bytes)
358
356
  is_default = default_persona_name == persona_name
359
357
  display_name = prefix_favourite(is_default ? bold { persona_name } : persona_name, favs[persona_name])
360
358
 
361
- table << [ display_name, size, tokens_size, ]
359
+ table << [ display_name, es.bytes_formatted, es.tokens_formatted, ]
362
360
  end
363
361
 
364
362
  table.align_column 1, :right
@@ -554,24 +552,40 @@ module OllamaChat::PersonaeManagement
554
552
  persona_name
555
553
  end
556
554
 
555
+ # Imports a character persona from JSON data, prompts for a name,
556
+ # and saves the resulting Markdown profile to disk.
557
+ #
558
+ # @param json_data [String] The raw character data in JSON format.
559
+ # @return [String, nil] The name of the created persona if successful,
560
+ # or nil if the process was cancelled or failed.
561
+ def import_persona_from_json(json_data)
562
+ persona_name = determine_valid_new_name_for_persona('to import from JSON/PNG') or return
563
+ markdown = convert_json_character_to_markdown(json_data, persona_name)
564
+ persona_pathname = persona_name_to_pathname(persona_name)
565
+ persona_pathname.write(markdown)
566
+ persona_name
567
+ end
568
+
557
569
  # Transforms raw character data (JSON or YAML) into a high-fidelity,
558
570
  # structured Markdown persona profile using the persona architect prompt and
559
571
  # the current persona template.
560
572
  #
561
573
  # This method leverages the LLM to interpret raw attributes and expand them
562
574
  # 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.
575
+ # standard persona structure. It also normalizes placeholders: {{user}} is
576
+ # converted to %{user} for runtime personalization, and {{char}} is replaced
577
+ # with the actual character name provided.
565
578
  #
566
579
  # @param character [String] the raw character data in JSON format
580
+ # @param persona_name [String] the name of the character to replace {{char}} with
567
581
  # @return [String] the resulting structured Markdown persona profile
568
- def convert_json_character_to_markdown(character)
582
+ def convert_json_character_to_markdown(character, persona_name)
569
583
  generate(
570
584
  prompt: prompt(:persona_architect).to_s % {
571
585
  character:,
572
586
  persona_template: prompt(:persona).to_s
573
587
  }
574
- ).response.gsub('{{user}}', '%{user}')
588
+ ).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.
@@ -232,24 +232,38 @@ module OllamaChat::PromptManagement
232
232
  # Interactively generates follow-up prompt suggestions based on the current
233
233
  # session.
234
234
  #
235
- # This method prompts the user to select a suggestion strategy (e.g., coding
236
- # or roleplaying), constructs a prompt containing the conversation history,
237
- # and requests a generation from the AI model. The resulting suggestions
238
- # are then opened in the editor for final refinement before being returned.
235
+ # This method constructs a prompt containing the conversation history and an
236
+ # instruction (either selected from a template or provided manually) and
237
+ # requests a generation from the AI model. The resulting suggestions are then
238
+ # opened in the editor for final refinement before being returned.
239
+ #
240
+ # @param edit [Boolean] If true, allows the user to write a custom suggestion
241
+ # instruction on the fly; otherwise, prompts the user to pick a template.
239
242
  #
240
243
  # @return [String, nil] The refined suggestion text, or nil if the process
241
244
  # was cancelled.
242
- def suggest_prompts
243
- # Let the user pick a prompt template (e.g., suggest_coding, suggest_roleplaying)
244
- instruction = choose_prompt(prompt: 'Which suggestion strategy shall we employ? %s') or return
245
+ def suggest_prompts(edit: false)
246
+ instruction = nil
247
+ if edit
248
+ # Let the user write a suggestion instruction on the fly
249
+ instruction = edit_text('')
250
+ else
251
+ # Let the user pick a prompt template (e.g., suggest_coding, suggest_roleplaying)
252
+ instruction = choose_prompt(prompt: 'Which suggestion strategy shall we employ? %s') or return
253
+ end
245
254
 
246
255
  # Build the context by gathering all current conversation messages
247
256
  history = prepare_conversation_history
248
- full_prompt = "Conversation History:\n#{history}\n\n#{instruction}"
257
+ full_prompt = <<~EOT
258
+ Conversation History:
259
+ #{history}
260
+
261
+ Instruction:
262
+ #{instruction}
263
+ EOT
249
264
 
250
- # Execute a silent generation call (doesn't add to history)
251
- response = generate(prompt: full_prompt)
252
- suggestions = response.response
265
+ # Execute a silent chat oneshot call (doesn't add to history)
266
+ suggestions = generate(prompt: full_prompt)
253
267
 
254
268
  # Pass the AI's suggestions through the editor for final refinement
255
269
  edit_text(suggestions)
@@ -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
@@ -353,7 +349,7 @@ module OllamaChat::SessionManagement
353
349
  prompt: prompt(:session_summarize).to_s % {
354
350
  sender_name:, unit:, message_content:, message_thinking:, context:
355
351
  }
356
- ).response
352
+ )
357
353
  content = if pretty
358
354
  '**%s**: %s' % [ sender_name_output, summary ]
359
355
  else
@@ -384,7 +380,7 @@ module OllamaChat::SessionManagement
384
380
  c << "%s: %s\n\n" % [ sender_name, message.content ]
385
381
  end
386
382
  prompt = prompt(:session_title).to_s % { length:, content: }
387
- generate(prompt:).response.full? do |name|
383
+ generate(prompt:).full? do |name|
388
384
  name = name.
389
385
  gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
390
386
  gsub(/\s+/, ' ')
@@ -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 % { source:, collection: }
250
250
  end
251
251
 
252
252
  private
@@ -241,6 +241,14 @@ module OllamaChat::StateSelectors
241
241
  # that manages the currently selected voice for the chat session
242
242
  attr_reader :voices
243
243
 
244
+ # The context_format reader returns the context format selector for the
245
+ # chat session.
246
+ #
247
+ # @return [ OllamaChat::StateSelector ] the context format selector object
248
+ # that manages the format used for project context generation
249
+ # (e.g., 'JSON', 'TOON')
250
+ attr_reader :context_format
251
+
244
252
  # Sets up state selectors for document policy and think mode based on the
245
253
  # provided configuration.
246
254
  #
@@ -268,5 +276,11 @@ module OllamaChat::StateSelectors
268
276
  states: config.voice.list,
269
277
  allow_empty: true
270
278
  )
279
+ @context_format = DatabaseStateSelector.new(
280
+ chat: self,
281
+ attribute: :context_format,
282
+ name: 'Context format',
283
+ states: %w[ JSON TOON ],
284
+ )
271
285
  end
272
286
  end
@@ -41,6 +41,27 @@ module OllamaChat::SystemPromptManagement
41
41
  messages.system_name
42
42
  end
43
43
 
44
+ # Resolves the currently active system prompt name to its raw text content.
45
+ #
46
+ # If the current system prompt is set to 'model_default', this method
47
+ # retrieves the default prompt associated with the loaded model. Otherwise,
48
+ # it fetches the content of the specifically named system prompt from the
49
+ # database.
50
+ #
51
+ # The returned string represents the base template; any dynamic placeholders
52
+ # (e.g., %{persona} and %{runtime_info}) are nullified to ensure only the raw
53
+ # structure is returned.
54
+ #
55
+ # @return [String] The raw text content of the active system prompt.
56
+ def raw_system_prompt
57
+ system_name = current_system_prompt_name
58
+ if system_name == 'model_default'
59
+ model_default_system_prompt.to_s
60
+ else
61
+ system_prompt(system_name).to_s
62
+ end % { persona: nil, runtime_info: nil }
63
+ end
64
+
44
65
  # Retrieves the content of the system prompt currently active in the chat
45
66
  # session.
46
67
  #
@@ -5,7 +5,6 @@
5
5
  # thinking modes, checking the current state, and displaying the current
6
6
  # think mode status.
7
7
  module OllamaChat::ThinkControl
8
-
9
8
  # An array of strings representing the valid configuration states for the
10
9
  # thinking mode.
11
10
  #
@@ -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
@@ -65,9 +65,10 @@ class OllamaChat::Tools::FileContext
65
65
  # @return [String] the generated context data in the configured format (JSON by default)
66
66
  # @return [String] a JSON string containing error information if the operation fails
67
67
  def execute(tool_call, **opts)
68
- config = opts[:chat].config
68
+ chat = opts[:chat]
69
+ config = chat.config
69
70
  pattern = tool_call.function.arguments.pattern
70
- format = config.context.format
71
+ format = chat.context_format.selected
71
72
 
72
73
  directory = Pathname.new(tool_call.function.arguments.directory || ?.)
73
74
  search_path = directory + pattern
@@ -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!',
@@ -181,7 +188,7 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
181
188
  begin
182
189
  # We use the active chat model to perform the surgical precision
183
190
  # filtering
184
- if response = chat.generate(prompt:)&.response.full?
191
+ if response = chat.generate(prompt:).full?
185
192
  indices = response.scan(/\d+/).map(&:to_i).select { |i| (0...records.size).include?(i) }
186
193
  records = records.values_at(*indices) if indices.any?
187
194
  end
@@ -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