ollama_chat 0.0.100 → 0.0.101

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +41 -0
  3. data/README.md +51 -1
  4. data/Rakefile +1 -0
  5. data/bin/ollama_chat_log +64 -0
  6. data/lib/ollama_chat/chat.rb +13 -4
  7. data/lib/ollama_chat/clipboard.rb +7 -2
  8. data/lib/ollama_chat/commands.rb +20 -4
  9. data/lib/ollama_chat/database/models/session.rb +20 -7
  10. data/lib/ollama_chat/favourites_management.rb +2 -0
  11. data/lib/ollama_chat/follow_chat.rb +43 -30
  12. data/lib/ollama_chat/history.rb +3 -0
  13. data/lib/ollama_chat/logging.rb +24 -3
  14. data/lib/ollama_chat/message_list.rb +11 -0
  15. data/lib/ollama_chat/model_handling.rb +1 -0
  16. data/lib/ollama_chat/prompt_handling.rb +5 -1
  17. data/lib/ollama_chat/prompt_management.rb +7 -1
  18. data/lib/ollama_chat/rag_handling.rb +9 -2
  19. data/lib/ollama_chat/session_management.rb +19 -7
  20. data/lib/ollama_chat/source_fetching.rb +20 -2
  21. data/lib/ollama_chat/tools/browse.rb +1 -1
  22. data/lib/ollama_chat/tools/compute_bmi.rb +2 -2
  23. data/lib/ollama_chat/tools/copy_to_clipboard.rb +1 -1
  24. data/lib/ollama_chat/tools/delete_file.rb +3 -2
  25. data/lib/ollama_chat/tools/directory_structure.rb +1 -1
  26. data/lib/ollama_chat/tools/eval_ruby.rb +2 -2
  27. data/lib/ollama_chat/tools/execute_grep.rb +5 -1
  28. data/lib/ollama_chat/tools/execute_ri.rb +1 -1
  29. data/lib/ollama_chat/tools/file_context.rb +1 -1
  30. data/lib/ollama_chat/tools/gem_path_lookup.rb +1 -1
  31. data/lib/ollama_chat/tools/generate_image.rb +1 -1
  32. data/lib/ollama_chat/tools/generate_password.rb +1 -1
  33. data/lib/ollama_chat/tools/get_current_weather.rb +1 -1
  34. data/lib/ollama_chat/tools/get_cve.rb +1 -1
  35. data/lib/ollama_chat/tools/get_endoflife.rb +1 -1
  36. data/lib/ollama_chat/tools/get_ghr.rb +1 -1
  37. data/lib/ollama_chat/tools/get_jira_issue.rb +1 -1
  38. data/lib/ollama_chat/tools/get_location.rb +1 -1
  39. data/lib/ollama_chat/tools/get_rfc.rb +1 -1
  40. data/lib/ollama_chat/tools/get_time.rb +1 -1
  41. data/lib/ollama_chat/tools/get_url.rb +1 -1
  42. data/lib/ollama_chat/tools/move_file.rb +3 -2
  43. data/lib/ollama_chat/tools/open_file_in_editor.rb +1 -1
  44. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
  45. data/lib/ollama_chat/tools/paste_into_editor.rb +1 -1
  46. data/lib/ollama_chat/tools/patch_file.rb +60 -42
  47. data/lib/ollama_chat/tools/read_file.rb +14 -5
  48. data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
  49. data/lib/ollama_chat/tools/retrieve_document_snippets.rb +6 -2
  50. data/lib/ollama_chat/tools/roll_dice.rb +1 -1
  51. data/lib/ollama_chat/tools/run_tests.rb +5 -2
  52. data/lib/ollama_chat/tools/search_web.rb +1 -1
  53. data/lib/ollama_chat/tools/write_file.rb +15 -11
  54. data/lib/ollama_chat/utils/log_viewer.rb +119 -0
  55. data/lib/ollama_chat/utils.rb +1 -0
  56. data/lib/ollama_chat/version.rb +1 -1
  57. data/lib/ollama_chat/web_searching.rb +3 -0
  58. data/ollama_chat.gemspec +7 -6
  59. data/spec/ollama_chat/chat_spec.rb +35 -8
  60. data/spec/ollama_chat/follow_chat_spec.rb +5 -0
  61. data/spec/ollama_chat/message_list_spec.rb +9 -0
  62. data/spec/ollama_chat/tools/patch_file_spec.rb +40 -12
  63. data/spec/ollama_chat/tools/read_file_spec.rb +9 -0
  64. data/spec/ollama_chat/utils/log_viewer_spec.rb +137 -0
  65. data/spec/ollama_chat/web_searching_spec.rb +2 -2
  66. metadata +22 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 436406ccfaa267c8996e6986759b3f79773ad8b48127ed8b7f7bd749b8c82dd1
4
- data.tar.gz: f2a66b87ebc221adf19e382568979f57baabfabef49929e123bbe0cc1932916d
3
+ metadata.gz: 55511ef08f35b482ac7529dae4b9e4d9cb490bd37467fac563b966f3ad87f569
4
+ data.tar.gz: 26846b60e0ef32214b21a1e56128b4be6dbe6bd0808f195c8130186c01f8975c
5
5
  SHA512:
6
- metadata.gz: 576fc144f38595bec62043b30c170dc65a2bb891df41ab36bdd1b4bef2ad32dbe0b72b413d0e0f785604c0c0e3add5ca847e6da25144011518d67241b7fb8771
7
- data.tar.gz: e3c808321e651a55bdf29326d41150700bcd2c8b28d4b2b407c49fdd6aff156e4e4d8555eafb1cf10c46589e44963714dafea95f667305c4173e788cd514da7f
6
+ metadata.gz: 1d7960e9303887e5ef3571b79bffbf2cd30785c8db4051e6fc7a16f828e7d5dbe8754006a5356074c83fb989dd35b27b5311d1e676c330f4303a81014d16c4ac
7
+ data.tar.gz: dfcb0fee96b6093e0d30cd7cb4894aa25752054236d85a14161519fb781d1697c7011816441dbdddfeb3477872c057b9382c594b2691c40f3238c9c727d43843
data/CHANGES.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-07-26 v0.0.101
4
+
5
+ ### Added
6
+
7
+ - Introduced a `clean` subcommand to `/conversation` to remove tool content,
8
+ images, and thinking from messages in place, including a confirmation prompt
9
+ for the destructive operation.
10
+ - Implemented `OllamaChat::Utils::LogViewer` and the `bin/ollama_chat_log`
11
+ utility for real-time, colorized JSON log tailing with regex filtering.
12
+ - Added `estimate_tokens` and `count_messages` helper methods to
13
+ `OllamaChat::Database::Models::Session`.
14
+ - Implemented `clean_messages!` in `MessageList` with associated YARD
15
+ documentation.
16
+
17
+ ### Changed
18
+
19
+ - Enhanced file patching integrity by replacing `mtime` and `line_count` checks
20
+ with a CRC32 `checksum`. This acts as a "proof of reading": since LLMs cannot
21
+ calculate CRC32 in their head, echoing the checksum proves they have the
22
+ latest file context including linenumbers without requiring cryptographic
23
+ security.
24
+ - Updated `read_file` to conditionally return `checksum` only when full-file
25
+ line numbers are requested, optimizing token usage for non-patching workflows.
26
+ - Refactored `OllamaChat::Logging#log` to use `deep_transform` for payload
27
+ sanitization, handling circular references and non-serializable objects.
28
+ - Standardized the `tool: name` variable across 28+ tool classes to ensure
29
+ consistent audit trails.
30
+ - Expanded logging coverage for file operations, grep, tests, RAG handling,
31
+ session management, and clipboard operations.
32
+ - Split `OllamaChat::FollowChat#eval_stats` into a data extraction method
33
+ `stats_hash` and a separate formatting method.
34
+ - Updated `/conversation` command to support the `-c` flag for `save` and added
35
+ path validation for `save` and `load` subcommands.
36
+ - Modified `lib/ollama_chat/tools/patch_file.rb` to validate that `edits` is an
37
+ `Array` and handle `checksum` mismatches.
38
+
39
+ ### Fixed
40
+
41
+ - Improved option parsing scope within the `/conversation` command by moving
42
+ logic inside the `when 'save'` block.
43
+
3
44
  ## 2026-07-23 v0.0.100
4
45
 
5
46
  ### New Features
data/README.md CHANGED
@@ -120,7 +120,7 @@ A source isn't just a filename; it can be any of the following:
120
120
  * **Remote URLs**: Any `http://` or `https://` link. The command will fetch the
121
121
  web content and parse it before importing.
122
122
  * **Shell Commands (`!`)**: Any source starting with an exclamation mark is
123
- executed as a shell command, and its **STDOUT** is imported as the content.
123
+ executed as a shell command, and its **STDOUT** is imported as the content.
124
124
  * *Example*: `/input !ls -la` imports the current directory listing.
125
125
  * *Example*: `/input !git branch` tells the AI which branch you are on.
126
126
 
@@ -358,6 +358,56 @@ functionality:
358
358
  These parameters provide greater flexibility in how you interact with
359
359
  `ollama_chat`, whether from the command line or integrated tools like `vim`.
360
360
 
361
+ ### Using `ollama_chat_log` to Inspect Application Logs
362
+
363
+ OllamaChat produces structured JSON logs that capture every tool execution,
364
+ conversation state, and internal event. The `ollama_chat_log` utility allows you
365
+ to filter, colorize, and tail these logs in real-time, displaying the full
366
+ structured data payload by default.
367
+
368
+ #### Basic Usage
369
+
370
+ By default, `ollama_chat_log` reads the current session's log file located at
371
+ `~/.local/state/ollama_chat/chat.json` (or wherever the default points).
372
+
373
+ ```bash
374
+ # Show the last 10 log entries
375
+ $ ollama_chat_log
376
+
377
+ # Show the last 50 entries
378
+ $ ollama_chat_log -n 50
379
+ ```
380
+
381
+ #### Real-time Tailing
382
+
383
+ Use the `-f` flag to follow the log file as new entries are appended, similar to
384
+ `tail -f`.
385
+
386
+ ```bash
387
+ $ ollama_chat_log -f
388
+ ```
389
+
390
+ #### Filtering Logs with `-m`
391
+
392
+ The `-m` flag allows you to filter logs based on nested JSON paths using dot
393
+ notation. You can match for key presence or substring matches within values.
394
+
395
+ - **Key Presence**: `-m "data.tool"` (shows only logs that have a `tool` key)
396
+ - **Substring Match**: `-m "data.tool=read_file"` (shows logs where the tool is `read_file`)
397
+ - **Nested Paths**: `-m "data.function.name=patch_file"`
398
+ - **Regexp Match**: `-m "data.tool=^(write_file|patch_file)$"`
399
+
400
+ You can chain multiple `-m` flags together. All patterns must match for a log
401
+ entry to be displayed (AND logic).
402
+
403
+ ```bash
404
+ # Show only successful file reads
405
+ $ ollama_chat_log -m "data.tool=read_file" -m "level=info"
406
+
407
+ # Follow and watch for specific tool executions
408
+ $ ollama_chat_log -f -m "data.tool=execute_grep"
409
+ ```
410
+
361
411
  ## Available Tools
362
412
 
363
413
  The assistant can interact with the system using a variety of tools to gather
data/Rakefile CHANGED
@@ -60,6 +60,7 @@ GemHadar do
60
60
  dependency 'infobar', '>= 0.13.1'
61
61
  dependency 'rubyzip', '~> 3.0'
62
62
  dependency 'sequel', '~> 5.0'
63
+ dependency 'file-tail', '~> 1.4'
63
64
  development_dependency 'all_images', '~> 0.12'
64
65
  development_dependency 'rspec', '~> 3.2'
65
66
  development_dependency 'kramdown', '~> 2.0'
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ollama_chat'
4
+ require 'file/tail'
5
+ require 'tins/xt'
6
+
7
+ opts = Tins::GO.go 'fm:n:h:', defaults: { ?n => 10 }
8
+ log_path = ARGV.shift || OC::OLLAMA::CHAT::LOGFILE
9
+
10
+ if opts[?h]
11
+ puts <<~EOT
12
+ Usage: #{File.basename($0)} [OPTIONS] [LOGFILE]
13
+
14
+ Options:
15
+ -f Follow the log file (real-time tailing)
16
+ -n NUMBER Rewind and show the last NUMBER of lines before starting
17
+ -m MATCH Filter logs by JSON key paths and values (can be used multiple times)
18
+ - Key presence match: -m "data.tool"
19
+ - Substring match: -m "data.tool=read_file" or -m "level=ERROR"
20
+ - Regexp match: -m "data.tool=^(write_file|patch_file)$"
21
+ - Nested path match: -m "data.function.name=write_file"
22
+ -h Show this help
23
+
24
+ LOGFILE Path to the JSON log file (defaults to the current chat log)
25
+ EOT
26
+ exit 0
27
+ end
28
+
29
+ unless File.exist?(log_path)
30
+ STDERR.puts "Error: Log file not found at #{log_path.inspect}"
31
+ exit 1
32
+ end
33
+
34
+ # The processing loop
35
+ def process_logs(path, follow: false, rewind: 0, display_data: true, match: nil)
36
+ if follow
37
+ # Real-time tailing implementation
38
+ puts "Watching logs... (Ctrl+C to stop)"
39
+ File::Tail::Logfile.open(path, backward: rewind) do |log|
40
+ log.tail do |line|
41
+ output = OllamaChat::Utils::LogViewer.format_line(line, display_data:, match:)
42
+ output or next
43
+ puts output
44
+ end
45
+ end
46
+ else
47
+ File::Tail::Logfile.open(path, backward: rewind) do |log|
48
+ log.each do |line|
49
+ output = OllamaChat::Utils::LogViewer.format_line(line, display_data:, match:)
50
+ output or next
51
+ puts output
52
+ end
53
+ end
54
+ end
55
+ rescue Interrupt
56
+ puts "\nStopping log viewer..."
57
+ end
58
+
59
+ process_logs(
60
+ log_path,
61
+ follow: opts[?f],
62
+ rewind: opts[?n].to_i,
63
+ match: opts[?m]
64
+ )
@@ -15,6 +15,7 @@ require 'csv'
15
15
  require 'socket'
16
16
  require 'shellwords'
17
17
  require 'context_spook'
18
+ require 'zlib'
18
19
 
19
20
  # A chat client for interacting with Ollama models through a terminal
20
21
  # interface.
@@ -160,9 +161,9 @@ class OllamaChat::Chat
160
161
  # @return [Array] a list of images to be sent with the next prompt
161
162
  attr_reader :images
162
163
 
163
- # Provides read-only access to the cache instance used by the object
164
+ # Provides read-only access to the cache instance used by the object.
164
165
  #
165
- # @attr_reader [Cache] the cache instance
166
+ # @return [OllamaChat::RedisCache, nil] the cache instance
166
167
  attr_reader :cache
167
168
 
168
169
  # The start method initializes the chat session by displaying information,
@@ -289,7 +290,13 @@ class OllamaChat::Chat
289
290
  # @param opts [Hash] keyword arguments for the chat call
290
291
  # @param block [Proc] optional handler block
291
292
  def call_ollama_chat(**opts, &block)
292
- log(:info, "Ollama chat request", data: { opts: })
293
+ if debug
294
+ log(:debug, "Ollama chat request sent", data: opts)
295
+ else
296
+ clean_opts = opts.dup
297
+ clean_opts = clean_opts.slice!(:messages, :tools)
298
+ log(:info, "Ollama chat request sent", data: clean_opts)
299
+ end
293
300
  ollama.chat(**opts, &block)
294
301
  end
295
302
 
@@ -414,6 +421,8 @@ class OllamaChat::Chat
414
421
  # The method also handles server socket messages, manages chat history, and
415
422
  # ensures proper cleanup and configuration handling throughout the
416
423
  # interaction.
424
+ #
425
+ # @return [Integer] returns 0 on successful completion or raises an error
417
426
  def interact_with_user
418
427
  loop do
419
428
  content = nil
@@ -634,7 +643,7 @@ class OllamaChat::Chat
634
643
  end
635
644
  end
636
645
 
637
- # Adds documents from command line arguments to the document collection
646
+ # Adds documents from command line arguments to the document collection.
638
647
  #
639
648
  # Processes a list of document paths or URLs, handling both local files and
640
649
  # remote resources.
@@ -109,14 +109,17 @@ module OllamaChat::Clipboard
109
109
  # message to standard error and does not re-raise the exception.
110
110
  #
111
111
  # @param edit [truthy/falsy] If true, opens the content in the editor for
112
- # modification before copying (default: true)
112
+ # modification before copying (default: false)
113
113
  #
114
114
  # @return [TrueClass] if the copying has been performed successfully.
115
115
  def copy_to_clipboard(edit: false)
116
- perform_copy_to_clipboard(edit:)
116
+ text = last_message_content(content: false)
117
+ perform_copy_to_clipboard(text: text, edit:)
118
+ log(:info, "Copied to clipboard", data: { bytes: format_bytes(text.bytesize) }) if text
117
119
  STDOUT.puts "The last response has been successfully copied to the system clipboard."
118
120
  true
119
121
  rescue OllamaChat::OllamaChatError => e
122
+ log(:error, e, data: { method: __method__ })
120
123
  STDERR.puts e.message
121
124
  end
122
125
 
@@ -133,9 +136,11 @@ module OllamaChat::Clipboard
133
136
  # or nil if an error occurred
134
137
  def paste_from_clipboard(edit: false)
135
138
  result = perform_paste_from_clipboard(edit:)
139
+ log(:info, "Pasted from clipboard", data: { bytes: format_bytes(result.bytesize) }) if result
136
140
  STDOUT.puts "The clipboard content has been successfully copied to the chat."
137
141
  result
138
142
  rescue OllamaChat::OllamaChatError => e
143
+ log(:error, e, data: { method: __method__ })
139
144
  STDERR.puts e.message
140
145
  end
141
146
  end
@@ -523,21 +523,37 @@ module OllamaChat::Commands
523
523
 
524
524
  command(
525
525
  name: :conversation,
526
- regexp: %r(^/conversation\s+(save|load)((?:\s+-(?:[c]))*)\s+([^-].*)$),
527
- complete: [ 'conversation', %w[ save load ] ],
526
+ regexp: %r(^/conversation\s+(clean|save|load)(\s+-c)?(?:\s+([^-].*))?$),
527
+ complete: [ 'conversation', %w[ save load clean ] ],
528
528
  options: '[-c]',
529
529
  help: <<~EOT
530
530
  💾 Save/Load conversation state
531
- (-c to clean first)
531
+ (-c to clean before saving)
532
+ Or clean inplace (removes tool content, images, and thinking)
532
533
  EOT
533
534
  ) do |subcommand,opts,path|
534
- opts = go_command('c', opts.to_s)
535
+ if %w[ save load ].include?(subcommand) && path.blank?
536
+ STDERR.puts "Require a path as argument to save/load!"
537
+ next :next
538
+ end
535
539
  case subcommand
536
540
  when 'save'
541
+ opts = go_command('c', opts.to_s)
537
542
  save_conversation(path, clean: opts[?c])
538
543
  when 'load'
539
544
  load_conversation(path)
540
545
  repair_group_uuids
546
+ when 'clean'
547
+ if confirm?(
548
+ prompt: '🔔 Clean tool content, images, and thinking from conversation? (y/n) ',
549
+ yes: /\Ay/i
550
+ )
551
+ then
552
+ messages.clean_messages!
553
+ STDOUT.puts "Conversation cleaned."
554
+ else
555
+ STDOUT.puts 'Cancelled.'
556
+ end
541
557
  end
542
558
  :next
543
559
  end
@@ -50,6 +50,21 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
50
50
  Tins::Duration.new(updated_at ? now - updated_at : 0)
51
51
  end
52
52
 
53
+ # Estimates the token and byte size of the session's message history.
54
+ #
55
+ # @return [OllamaChat::TokenEstimator::Estimate] the estimated token and byte counts
56
+ def estimate_tokens
57
+ size_bytes = messages.to_s.size
58
+ OllamaChat::TokenEstimator.estimate(size_bytes)
59
+ end
60
+
61
+ # Counts the number of messages in the session's JSONL history.
62
+ #
63
+ # @return [Integer] the number of messages stored in the session
64
+ def count_messages
65
+ messages.to_s.count(?\n)
66
+ end
67
+
53
68
  # @!attribute [v] id
54
69
  # @return [Integer] The primary key for the session.
55
70
  #
@@ -128,13 +143,11 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
128
143
  # @!attribute [v] updated_at
129
144
  # @return [Time, nil] The timestamp of the last update to the session.
130
145
 
131
- # The with_defaults method is a factory method that initializes a new
132
- # Session model instance with a set of default values derived
133
- # from the
134
- # @param chat [OllamaChat::Chat] the active chat instance used to
135
- # extract
136
- # @return [OllamaChat::Database::Models::Session] a new session
137
- # instance with default attributes
146
+ # Factory method that initializes a new Session model instance with a set
147
+ # of default values derived from the active chat configuration.
148
+ #
149
+ # @param chat [OllamaChat::Chat] the active chat instance used to extract defaults
150
+ # @return [OllamaChat::Database::Models::Session] a new session instance with default attributes
138
151
  def self.with_defaults(chat)
139
152
  tools_default_enabled =
140
153
  chat.config.tools.functions.to_h.
@@ -54,6 +54,7 @@ module OllamaChat::FavouritesManagement
54
54
  return
55
55
  when SearchUI::Wrapper
56
56
  models::Favourite.create(context: type, name: chosen.value)
57
+ log(:info, "Favourite added", data: { context: type, name: chosen.value })
57
58
  end
58
59
  end
59
60
  end
@@ -80,6 +81,7 @@ module OllamaChat::FavouritesManagement
80
81
  return
81
82
  when SearchUI::Wrapper
82
83
  models::Favourite.where(context: type, name: chosen.value).destroy
84
+ log(:info, "Favourite removed", data: { context: type, name: chosen.value })
83
85
  end
84
86
  end
85
87
  end
@@ -15,12 +15,13 @@ class OllamaChat::FollowChat
15
15
 
16
16
  # Initializes a new instance of OllamaChat::FollowChat.
17
17
  #
18
- # @param [OllamaChat::Chat] chat The chat object, which represents the
18
+ # @param chat [OllamaChat::Chat] The chat object, which represents the
19
19
  # conversation context.
20
- # @param [#to_a] messages A collection of message objects, representing the
20
+ # @param messages [#to_a] A collection of message objects, representing the
21
21
  # conversation history.
22
- # @param [String] voice (optional) to speek with if any.
23
- # @param [IO] output (optional) The output stream where terminal output
22
+ # @param group_uuid [String, nil] The group UUID for the conversation.
23
+ # @param voice [String, nil] (optional) The voice to speak with.
24
+ # @param output [IO] (optional) The output stream where terminal output
24
25
  # should be printed. Defaults to STDOUT.
25
26
  #
26
27
  # @return [OllamaChat::FollowChat] A new instance of OllamaChat::FollowChat.
@@ -167,21 +168,20 @@ class OllamaChat::FollowChat
167
168
  )
168
169
  result = OllamaChat::Tools.registered[name].execute(tool_call, chat:)
169
170
  if confirmed == :explicit
170
- chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmation: :explicit })
171
+ chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
171
172
  else
172
- chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmation: :implicit })
173
+ chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
173
174
  end
174
175
  end
175
176
 
176
177
  chat.tool_call_results[name] << result
177
-
178
178
  data = nil
179
179
  message = begin
180
180
  data = JSON.parse(result)
181
- chat.log(:info, "Tool result", data:)
182
- data['message']
181
+ chat.log(:info, "Tool result", data: { result: data })
182
+ data.to_hash['message'] if data.respond_to?(:to_hash)
183
183
  rescue
184
- chat.log(:info, "Tool result", data: { raw_result: result })
184
+ chat.log(:info, "Tool result", data: { result: })
185
185
  nil
186
186
  end
187
187
  warn =
@@ -346,39 +346,52 @@ class OllamaChat::FollowChat
346
346
  end
347
347
  end
348
348
 
349
- # The eval_stats method processes response statistics and formats them into a
350
- # colored, readable string output.
349
+ # Extracts performance statistics from an Ollama response and formats them
350
+ # into a hash of human-readable strings and metrics.
351
351
  #
352
- # @param response [ Object ] the response object containing evaluation metrics
353
- #
354
- # @return [ String ] a formatted string with statistical information about
355
- # the evaluation process including durations, counts, and rates, styled
356
- # with colors and formatting
357
- def eval_stats(response)
352
+ # @param response [Object] the response object containing evaluation metrics
353
+ # @return [Hash] a hash of formatted statistics including durations, counts, and rates
354
+ def stats_hash(response)
358
355
  eval_duration = response.eval_duration.to_f / 1e9
359
356
  prompt_eval_duration = response.prompt_eval_duration.to_f / 1e9
360
- stats_text = {
361
- eval_duration: Tins::Duration.new(eval_duration),
357
+ {
358
+ eval_duration: Tins::Duration.new(eval_duration).to_s,
362
359
  eval_count: response.eval_count.to_i,
363
- eval_rate: bold { "%.2f t/s" % (response.eval_count.to_i / eval_duration) } + color(111),
364
- prompt_eval_duration: Tins::Duration.new(prompt_eval_duration),
360
+ eval_rate: "%.2f t/s" % (response.eval_count.to_i / eval_duration),
361
+ prompt_eval_duration: Tins::Duration.new(prompt_eval_duration).to_s,
365
362
  prompt_eval_count: response.prompt_eval_count.to_i,
366
- prompt_eval_rate: bold { "%.2f t/s" % (response.prompt_eval_count.to_i / prompt_eval_duration) } + color(111),
367
- total_duration: Tins::Duration.new(response.total_duration / 1e9),
368
- load_duration: Tins::Duration.new(response.load_duration / 1e9),
369
- }.map { _1 * ?= } * ' '
363
+ prompt_eval_rate: "%.2f t/s" % (response.prompt_eval_count.to_i / prompt_eval_duration),
364
+ total_duration: Tins::Duration.new(response.total_duration / 1e9).to_s,
365
+ load_duration: Tins::Duration.new(response.load_duration / 1e9).to_s,
366
+ }
367
+ end
368
+
369
+ # The eval_stats method processes a statistics hash and formats it into a
370
+ # colored, readable string output.
371
+ #
372
+ # @param stats [Hash] the statistics hash containing evaluation metrics
373
+ #
374
+ # @return [String] a formatted string with statistical information about
375
+ # the evaluation process including durations, counts, and rates, styled
376
+ # with colors and formatting
377
+ def eval_stats(stats)
378
+ stats_text = stats.
379
+ deep_transform(value: -> idx, v { idx =~ /_rate\z/ ? bold(v) + color(111) : v }).
380
+ map { _1 * ?= } * ' '
370
381
  '📊 ' + color(111) {
371
382
  Kramdown::ANSI::Width.wrap(stats_text, percentage: 90).gsub(/(?<!\A)^/, ' ')
372
383
  }
373
384
  end
374
385
 
375
- # The output_eval_stats method outputs evaluation statistics to the specified
376
- # output stream.
386
+ # The output_eval_stats method extracts performance statistics from the response,
387
+ # logs them to the chat history, and prints a formatted summary to the terminal.
377
388
  #
378
- # @param response [ Object ] the response object containing evaluation data
389
+ # @param response [Object] the response object containing evaluation data
379
390
  def output_eval_stats(response)
380
391
  response.done or return
381
- @output.puts "", eval_stats(response)
392
+ stats = stats_hash(response)
393
+ chat.log(:info, 'Ollama chat response received', data: { stats: })
394
+ @output.puts "", eval_stats(stats)
382
395
  end
383
396
 
384
397
  # The debug_output method conditionally outputs the response object using jj
@@ -81,6 +81,7 @@ module OllamaChat::History
81
81
  read_io(input:).to_a
82
82
  Reline::HISTORY.clear
83
83
  Reline::HISTORY.push(*history_data)
84
+ log(:info, "History initialized", data: { entries: history_data.size })
84
85
  end
85
86
  end
86
87
 
@@ -96,6 +97,7 @@ module OllamaChat::History
96
97
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').
97
98
  write_io(output:, collection: Reline::HISTORY)
98
99
  session.history = output.string
100
+ log(:info, "History saved", data: { entries: Reline::HISTORY.size })
99
101
  end
100
102
  end
101
103
 
@@ -113,6 +115,7 @@ module OllamaChat::History
113
115
  Reline::HISTORY.clear
114
116
  ensure
115
117
  save_history
118
+ log(:info, "History cleared")
116
119
  end
117
120
  end
118
121
  end
@@ -31,15 +31,36 @@ module OllamaChat::Logging
31
31
  payload = {
32
32
  msg: ,
33
33
  data: data || {}
34
- }
34
+ }.symbolize_keys_recursive
35
35
 
36
36
  if msg.is_a?(Exception)
37
+ if backtrace = msg.ask_and_send(:backtrace)
38
+ payload[:data][:backtrace] = backtrace
39
+ end
37
40
  payload[:msg] = msg = "#{msg.class}: #{msg.message}"
38
- payload[:data][:backtrace] = msg.ask_and_send(:backtrace)
39
41
  else
40
- payload[:msg] = msg.to_s
42
+ payload[:msg] = msg = msg.to_s
41
43
  end
42
44
 
45
+ # Sanitize payload for safe JSON serialization: break circular references
46
+ # and coerce non-serializable objects to strings while preserving
47
+ # primitives/hashes/arrays
48
+ payload = payload.deep_transform(
49
+ circular: ?…,
50
+ value: -> node {
51
+ keep = [
52
+ responding?(:to_hash), responding?(:to_ary), responding?(:to_int),
53
+ true, false, nil,
54
+ ]
55
+ case node
56
+ when *keep
57
+ node
58
+ else
59
+ node.to_s
60
+ end
61
+ }
62
+ )
63
+
43
64
  logger.send(severity, payload)
44
65
  warn and self.warn(msg)
45
66
  nil
@@ -204,6 +204,17 @@ class OllamaChat::MessageList
204
204
  end
205
205
  end
206
206
 
207
+ # Cleans the messages in the list by replacing them with stripped versions.
208
+ # This is a destructive (mutating) operation.
209
+ #
210
+ # @param messages [Array<OllamaChat::Message>] the messages to clean.
211
+ # Defaults to all current messages.
212
+ # @return [OllamaChat::MessageList] self to allow for method chaining.
213
+ def clean_messages!(messages: @messages)
214
+ @messages = clean_messages(messages:)
215
+ self
216
+ end
217
+
207
218
  # Displays the most recent messages from the conversation history.
208
219
  #
209
220
  # This method prints a specified number of trailing messages to the console
@@ -342,6 +342,7 @@ module OllamaChat::ModelHandling
342
342
 
343
343
  old_model != @model and reconfigure_model_options(profile:, keep_options:)
344
344
 
345
+ log(:info, "Model switched", data: { old_model:, new_model: @model, profile: })
345
346
  @model_metadata
346
347
  end
347
348
 
@@ -88,7 +88,11 @@ module OllamaChat::PromptHandling
88
88
  patterns = Array(patterns.full? || '**/*.{txt,md}')
89
89
  filename = choose_filename(patterns)
90
90
 
91
- filename.read if filename&.exist?
91
+ content = filename.read if filename&.exist?
92
+ log(:info, "Prompt loaded from file", data: {
93
+ file: filename&.to_s, bytes: content&.size
94
+ })
95
+ content
92
96
  end
93
97
 
94
98
 
@@ -92,7 +92,8 @@ module OllamaChat::PromptManagement
92
92
  end
93
93
 
94
94
  prompt_content = edit_text(content)
95
- store_prompt(name, prompt_content, context:).to_s
95
+ store_prompt(name, prompt_content, context:)
96
+ log(:info, "Prompt added", data: { name:, context: })
96
97
  true
97
98
  end
98
99
  end
@@ -114,6 +115,7 @@ module OllamaChat::PromptManagement
114
115
  yes: /\Ay/i
115
116
  ) or return
116
117
  selected_prompt.destroy
118
+ log(:info, "Prompt deleted", data: { name: selected_prompt.name, context: })
117
119
  end
118
120
 
119
121
  # Interactively selects an existing prompt and allows the user to edit its
@@ -125,6 +127,7 @@ module OllamaChat::PromptManagement
125
127
  selected_prompt = choose_prompt(context:, prompt: 'Which spell needs some fine-tuning? %s') or return
126
128
  selected_prompt.metadata['content'] = edit_text(selected_prompt.metadata['content'].to_s)
127
129
  selected_prompt.save
130
+ log(:info, "Prompt edited", data: { name: selected_prompt.name, context: })
128
131
  self
129
132
  end
130
133
 
@@ -164,6 +167,7 @@ module OllamaChat::PromptManagement
164
167
  duplicated_prompt.name = name
165
168
  duplicated_prompt.metadata['default'] = false
166
169
  duplicated_prompt.save
170
+ log(:info, "Prompt duplicated", data: { name:, old_name: selected_prompt.name, context: })
167
171
  self
168
172
  end
169
173
 
@@ -197,6 +201,7 @@ module OllamaChat::PromptManagement
197
201
  prompt_name = determine_valid_new_name_for_prompt('to import', context:) or return
198
202
  prompt_content = filename.read
199
203
  store_prompt(prompt_name, prompt_content, context:)
204
+ log(:info, "Prompt imported", data: { name: prompt_name, source: filename.to_s, context: })
200
205
  STDOUT.puts "Imported prompt as #{prompt_name.inspect}."
201
206
  self
202
207
  end
@@ -220,6 +225,7 @@ module OllamaChat::PromptManagement
220
225
  )
221
226
  filename = determine_valid_output_filename('to write to') or return
222
227
  filename.write(selected_prompt.to_s)
228
+ log(:info, "Prompt exported", data: { name: selected_prompt.name, dest: filename.to_s, context: })
223
229
  STDOUT.puts "Prompt #{selected_prompt.name.inspect} was exported as #{filename.to_path.inspect}?"
224
230
  self
225
231
  end