ollama_chat 0.0.123 → 0.0.125

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +155 -0
  3. data/Rakefile +2 -2
  4. data/bin/ollama_chat_log +1 -1
  5. data/lib/ollama_chat/asr.rb +3 -3
  6. data/lib/ollama_chat/chat.rb +21 -27
  7. data/lib/ollama_chat/clipboard.rb +4 -4
  8. data/lib/ollama_chat/collection_search.rb +162 -0
  9. data/lib/ollama_chat/commands.rb +85 -55
  10. data/lib/ollama_chat/compaction.rb +36 -15
  11. data/lib/ollama_chat/config_handling.rb +13 -7
  12. data/lib/ollama_chat/conversation.rb +8 -5
  13. data/lib/ollama_chat/database/migrations/011_add_trigger_to_sessions.rb +7 -0
  14. data/lib/ollama_chat/database/models/app_state.rb +7 -5
  15. data/lib/ollama_chat/database/models/session.rb +15 -2
  16. data/lib/ollama_chat/database/session_locking.rb +3 -2
  17. data/lib/ollama_chat/dialog.rb +8 -8
  18. data/lib/ollama_chat/favourites_management.rb +4 -4
  19. data/lib/ollama_chat/feedback.rb +41 -0
  20. data/lib/ollama_chat/file_editing.rb +5 -4
  21. data/lib/ollama_chat/follow_chat.rb +33 -28
  22. data/lib/ollama_chat/information.rb +66 -15
  23. data/lib/ollama_chat/input_content.rb +5 -5
  24. data/lib/ollama_chat/message_editing.rb +3 -3
  25. data/lib/ollama_chat/message_list.rb +69 -13
  26. data/lib/ollama_chat/message_output.rb +7 -7
  27. data/lib/ollama_chat/model_handling.rb +71 -45
  28. data/lib/ollama_chat/oc.rb +3 -1
  29. data/lib/ollama_chat/ollama_chat_config/default_config.yml +48 -8
  30. data/lib/ollama_chat/parsing.rb +7 -7
  31. data/lib/ollama_chat/personae_management.rb +45 -27
  32. data/lib/ollama_chat/prompt_management.rb +54 -50
  33. data/lib/ollama_chat/rag_handling.rb +87 -25
  34. data/lib/ollama_chat/server_socket.rb +1 -1
  35. data/lib/ollama_chat/session_management.rb +146 -55
  36. data/lib/ollama_chat/source_fetching.rb +9 -7
  37. data/lib/ollama_chat/speaker.rb +15 -1
  38. data/lib/ollama_chat/state_selectors.rb +3 -2
  39. data/lib/ollama_chat/switches.rb +1 -3
  40. data/lib/ollama_chat/system_prompt_management.rb +14 -2
  41. data/lib/ollama_chat/tool_calling.rb +5 -5
  42. data/lib/ollama_chat/tools/concern.rb +1 -0
  43. data/lib/ollama_chat/tools/directory_structure.rb +6 -6
  44. data/lib/ollama_chat/tools/execute_shell.rb +1 -1
  45. data/lib/ollama_chat/tools/forget.rb +113 -0
  46. data/lib/ollama_chat/tools/memorize.rb +116 -0
  47. data/lib/ollama_chat/tools/roll_dice.rb +1 -1
  48. data/lib/ollama_chat/tools/run_tests.rb +1 -1
  49. data/lib/ollama_chat/tools/search_knowledge.rb +12 -60
  50. data/lib/ollama_chat/tools.rb +2 -0
  51. data/lib/ollama_chat/utils/audio_player.rb +1 -0
  52. data/lib/ollama_chat/utils/tag_resolver.rb +1 -1
  53. data/lib/ollama_chat/version.rb +1 -1
  54. data/lib/ollama_chat/vim.rb +4 -2
  55. data/lib/ollama_chat/web_searching.rb +8 -8
  56. data/lib/ollama_chat.rb +2 -0
  57. data/ollama_chat.gemspec +7 -7
  58. data/spec/ollama_chat/asr_spec.rb +5 -5
  59. data/spec/ollama_chat/chat_spec.rb +3 -3
  60. data/spec/ollama_chat/clipboard_spec.rb +3 -3
  61. data/spec/ollama_chat/collection_search_spec.rb +134 -0
  62. data/spec/ollama_chat/command_concern_spec.rb +174 -0
  63. data/spec/ollama_chat/commands_spec.rb +354 -6
  64. data/spec/ollama_chat/compaction_spec.rb +21 -15
  65. data/spec/ollama_chat/config_handling_spec.rb +9 -12
  66. data/spec/ollama_chat/conversation_spec.rb +17 -18
  67. data/spec/ollama_chat/favourites_management_spec.rb +6 -6
  68. data/spec/ollama_chat/file_editing_spec.rb +3 -3
  69. data/spec/ollama_chat/follow_chat_spec.rb +3 -1
  70. data/spec/ollama_chat/http_handling_spec.rb +10 -10
  71. data/spec/ollama_chat/information_spec.rb +4 -4
  72. data/spec/ollama_chat/input_content_spec.rb +1 -1
  73. data/spec/ollama_chat/message_editing_spec.rb +5 -5
  74. data/spec/ollama_chat/message_list_spec.rb +76 -2
  75. data/spec/ollama_chat/message_output_spec.rb +7 -7
  76. data/spec/ollama_chat/parsing_spec.rb +5 -5
  77. data/spec/ollama_chat/prompt_management_spec.rb +12 -14
  78. data/spec/ollama_chat/rag_handling_spec.rb +31 -32
  79. data/spec/ollama_chat/session_management_spec.rb +82 -11
  80. data/spec/ollama_chat/source_fetching_spec.rb +4 -4
  81. data/spec/ollama_chat/speaker_spec.rb +92 -0
  82. data/spec/ollama_chat/state_selectors_spec.rb +1 -1
  83. data/spec/ollama_chat/system_prompt_management_spec.rb +4 -4
  84. data/spec/ollama_chat/think_control_spec.rb +2 -2
  85. data/spec/ollama_chat/tool_calling_spec.rb +4 -4
  86. data/spec/ollama_chat/tools/execute_shell_spec.rb +7 -7
  87. data/spec/ollama_chat/tools/forget_spec.rb +144 -0
  88. data/spec/ollama_chat/tools/memorize_spec.rb +204 -0
  89. data/spec/ollama_chat/tools/resolve_tag_spec.rb +5 -9
  90. data/spec/ollama_chat/tools/search_knowledge_spec.rb +24 -19
  91. data/spec/ollama_chat/tts_spec.rb +13 -19
  92. data/spec/ollama_chat/utils/audio_player_spec.rb +5 -3
  93. data/spec/ollama_chat/web_searching_spec.rb +2 -2
  94. metadata +25 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2d0c5676e03433f245a21795872a9d7bd64357306ec691de3012195b5403bef
4
- data.tar.gz: 933dca2b53647c523b9849d958cc1625106c97500caf2814bc731458a0e683f4
3
+ metadata.gz: 3de94c682281a4667e89feee589883bd33acf082165f37ce443072117a773167
4
+ data.tar.gz: 94cccbb92af48198a70879cf1028a0aebfe0cda49e25ccd82352cb35d9054bf7
5
5
  SHA512:
6
- metadata.gz: 4589b5f1cf689bb35bdf26c288e59f40b87ef926eaf7f91e187c3fa729958c9d042f784eb3bf9a4b6e64966c201312713c2e20607291c6aabc10d306cf4cf272
7
- data.tar.gz: 19102c3d01a2ebb4bb14b8ea617b09a7e168d0175225024a490dc85e23b41ce3c7eba249d82cc671f5530da0090e945f8f6358d17ca7f6b7587a413002e27dc2
6
+ metadata.gz: d3bad30e34ad309fe6c2f2263802af2778eded9459affc6c6713c6383f58706fe54411e7a4b6f087c487718d0a6ea8e07e90dfed22ca1a64b563d207ce61287e
7
+ data.tar.gz: 8f9befaa4a6a519b8f7b4424f0d16380663b863e039f08142fc667b01ecb83aa989b9e635ea5373b22191c7ba8f568e97dd210d732d1d6d5099dd3e5cffb74ee
data/CHANGES.md CHANGED
@@ -1,5 +1,160 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-03 v0.0.125
4
+
5
+ ### Added
6
+
7
+ * Session-scoped trigger injection mechanism:
8
+ * New migration `011_add_trigger_to_sessions` adding a `:text` JSON column to
9
+ `sessions`.
10
+ * `Session#trigger` attribute with `plugin :serialization, :json` and `nil`
11
+ default.
12
+ * `CollectionSearch#trigger_inject` dispatch logic including embedding
13
+ guards, collection switching, document finding, and reranking.
14
+ * New `/session trigger edit` command registered in `commands.rb`.
15
+ * `edit_session_trigger` method in `session_management.rb` supporting dual
16
+ chooser and prefill.
17
+ * Trigger configuration display (⚡ block) added to `info_rag` in
18
+ `information.rb`.
19
+ * `runtime_information_inject` extracted from `chat.rb` into
20
+ `information.rb`.
21
+ * New prompt contexts: `prompts.rerank` (`default`, `memory_rerank`,
22
+ `lorebook_rerank`), `prompts.prompt.snippets_retrieval`, and
23
+ `prompts.prompt.snippets_trigger`.
24
+ * `persona_name` line added to `prompts.system.persona`.
25
+ * `TagResult` changed from `Data.define` to `Struct.new` to allow mutable
26
+ fields.
27
+ * `∅` fallback for empty patterns in `collection_stats`.
28
+ * Info-level logging for embedding requests/responses and rerank survivors in
29
+ `collection_search.rb`.
30
+ * New spec file `collection_search_spec.rb` with 7 examples for
31
+ `#trigger_inject`.
32
+
33
+ ### Changed
34
+
35
+ * `SessionLocking#lock?` now accepts a required `chat:` keyword argument to
36
+ decouple feedback delegation from the host object.
37
+ * `search_knowledge.rb` updated to use `snippets_retrieval` DB prompt,
38
+ `collection` block, and `database_collection?` guard.
39
+ * `@embedding` instance variable swept to `embedding` accessor in
40
+ `information.rb`, `source_fetching.rb`, and `web_searching.rb`.
41
+ * `Tins::Duration#format` used for compaction voice timing in
42
+ `report_compaction`, replacing `%.1f seconds`.
43
+ * `tins` dependency bumped from `~> **1.58**` to `~> **1.60**` to support
44
+ unpadded `%H`, `%M`, and `%u` directives.
45
+ * `resolve_tag_spec.rb` updated to replace an unreachable rescue-path test with
46
+ a "tagged file does not exist" test.
47
+ * `search_knowledge_spec.rb` updated for new prompt names, `collection` block,
48
+ and `tags_set` doubles.
49
+ * `compaction_spec.rb` regex updated to match the new duration format.
50
+ * `Tins::GO.go` flag string reordered in `bin/ollama_chat_log`.
51
+
52
+ ### Fixed
53
+
54
+ * Spec expectations in `session_management_spec.rb`, `parsing_spec.rb`, and
55
+ `source_fetching_spec.rb` updated to use Ruby 3.1+ hash shorthand `chat:`.
56
+
57
+ ### Tests
58
+
59
+ * Added specs for `CommandConcern` class methods:
60
+ * `help_message` via `OllamaChat::Commands`: table structure, category
61
+ headers, pattern filtering, subcommand expansion, and optional/mandatory
62
+ asterisks.
63
+ * `command_completions`: array shape, bare names, subcommand variants, and
64
+ sort order.
65
+ * `Command` class: `execute_if_match?` (match, captures, nil-regexp path,
66
+ no-context error), `completions` (product, optional bare, strict),
67
+ `command_names`, and `optional?`.
68
+ * Duplicate registration guard against the real `:copy` entry in
69
+ `OllamaChat::Commands`.
70
+ * Added specs for remaining command dispatch gaps:
71
+ * `/paste -i` stdin path via `paste_from_stdin`.
72
+ * `/last -v` voice output via `speak_last`.
73
+ * `/regenerate` success path (drop + return content).
74
+ * `/prompt delete` routes to `choose_and_delete_prompt`.
75
+ * `/web` without explicit count passes `nil`.
76
+ * `/character` JSON success paths: `info` (pager), `load` (raw content),
77
+ `import` (delegation).
78
+ * `/character` PNG `load` with `PNGMetadataExtractor`.
79
+ * Added specs for `SessionManagement#edit_session_trigger`:
80
+ * Setting trigger from `nil` to a configured hash with `enabled`,
81
+ `prompt_name`, and `text_count`.
82
+ * Updating config for an already-configured collection.
83
+ * Switching the trigger to a different collection name.
84
+ * Added specs for session model options and trigger edit:
85
+ * `/session model options` delegates to `edit_session_model_options` and
86
+ returns `:next`.
87
+ * `/session trigger edit` delegates to `edit_session_trigger` and returns
88
+ `:next`.
89
+
90
+ ## 2026-10-02 v0.0.124
91
+
92
+ ### Added
93
+
94
+ * `OllamaChat::Feedback` module to replace raw `STDOUT.puts` and
95
+ `STDERR.puts` calls with typed, testable system notifications.
96
+ * Supports types: `:success`, `:cancel`, `:denied`, `:warn`, `:info`,
97
+ `:alarm`.
98
+ * Integrates with `use_pager` and handles newline formatting via
99
+ `output.print`.
100
+ * `memorize` tool to embed timestamped entries into per-persona
101
+ `memory-<persona_name>` Documentrix collections.
102
+ * `forget` tool to clear a single memory record by its ISO-8601 timestamp
103
+ tag.
104
+ * `/collection query` subcommand to debug collections, supporting optional
105
+ `-e` (edit) and `-r` (rerank) flags.
106
+ * `/config env` subcommand to display the Ollama Chat environment variable
107
+ tree using `OC.view`.
108
+ * `/last -v` flag to voice previous assistant messages via
109
+ `MessageList#speak_last`.
110
+ * `OllamaChat::CollectionSearch` module extracted from `SearchKnowledge`
111
+ tool, providing `find_document_records` and `rerank_records` methods mixed
112
+ into `Chat`.
113
+ * `refresh_system_prompt` method in `SystemPromptManagement` to
114
+ re-interpolate the active system prompt with fresh `persona` and
115
+ `runtime_info` values after collection mutations.
116
+ * Voice announcement in the compaction report, including
117
+ `context_filled_before`, `context_filled_after`, and `duration` in
118
+ `Compaction::Result`.
119
+ * `Speaker#wait_for_speaker` method to join the speaker thread and block
120
+ until completion.
121
+ * `spec/ollama_chat/speaker_spec.rb` with tests for `OllamaChat::Speaker`.
122
+
123
+ ### Changed
124
+
125
+ * Replaced `str % {…}` with `named_placeholders_interpolate({…})` for
126
+ DB-prompt call sites to handle unknown placeholders gracefully.
127
+ * Updated `directory_structure` tool description from "Tree viewer" to "File
128
+ locator" and added guidance to prefer it over shell `find` or `ls -R`.
129
+ * Routed prompt change feedback through the `chat` instance in `app_state.rb`
130
+ to ensure messages target the active chat session.
131
+ * Switched spec stubs from `allow` to `expect` in `http_handling_spec.rb`,
132
+ `message_list_spec.rb`, `execute_shell_spec.rb`, `tts_spec.rb`,
133
+ `compaction_spec.rb`, `audio_player_spec.rb`, and `asr_spec.rb` to enforce
134
+ required calls.
135
+ * Bumped `const_conf` dependency from `~> **0.3**` to `~> **0.9**`.
136
+ * Updated `rerank_records` to accept an optional `prompt_name:` keyword
137
+ argument.
138
+ * Improved new session name generation by extracting
139
+ `new_random_session_name` into `SessionManagement` and handling `nil`
140
+ cancellation in `determine_valid_new_name_for_session`.
141
+ * Allowed naming new sessions via `choose_session` by threading an optional
142
+ `name:` keyword through `new_session` and `Session.with_defaults`.
143
+ * Logged stored memory text in the `memorize` tool by adding `text:` to the
144
+ `chat.log` data hash.
145
+ * Extracted context usage helpers `context_usage_colored` and
146
+ `context_percentage` to eliminate duplicated format logic.
147
+ * Updated `output_eval_stats` in `follow_chat.rb` to print colored context
148
+ usage.
149
+
150
+ ### Fixed
151
+
152
+ * Fixed indentation bug in `model_handling.rb` import/export by changing
153
+ `sub` to `gsub`.
154
+ * Guarded source link rendering in `search_knowledge` against `nil` or empty
155
+ `source` values.
156
+ * Suppressed `report_on_exception` on `AudioPlayer` background thread.
157
+
3
158
  ## 2026-09-25 v0.0.123
4
159
 
5
160
  ### Added
data/Rakefile CHANGED
@@ -49,13 +49,13 @@ GemHadar do
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
50
  dependency 'kramdown-ansi', '~> 0.5'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
- dependency 'tins', '~> 1.58'
52
+ dependency 'tins', '~> 1.60'
53
53
  dependency 'search_ui', '>= 0.2'
54
54
  dependency 'amatch', '~> 0.4'
55
55
  dependency 'pdf-reader', '~> 2.0'
56
56
  dependency 'bigdecimal', '~> 3.1'
57
57
  dependency 'csv', '~> 3.0'
58
- dependency 'const_conf', '~> 0.3'
58
+ dependency 'const_conf', '~> 0.9'
59
59
  dependency 'context_spook', '~> 1.6'
60
60
  dependency 'infobar', '>= 0.13.1'
61
61
  dependency 'rubyzip', '~> 3.0'
data/bin/ollama_chat_log CHANGED
@@ -4,7 +4,7 @@ require 'ollama_chat'
4
4
  require 'file/tail'
5
5
  require 'tins/xt'
6
6
 
7
- opts = Tins::GO.go 'fm:n:h:', defaults: { ?n => 10 }
7
+ opts = Tins::GO.go 'hfm:n:', defaults: { ?n => 10 }
8
8
  log_path = ARGV.shift || OC::OLLAMA::CHAT::LOG::CHAT
9
9
 
10
10
  if opts[?h]
@@ -38,11 +38,11 @@ module OllamaChat
38
38
  executable = Shellwords.split(cmd).first
39
39
  result = system(cmd, out: File::NULL, err: File::NULL)
40
40
  if result.nil?
41
- STDERR.puts "ASR: #{executable} not found in PATH."
41
+ chat.feedback("ASR: #{executable} not found in PATH.", type: :warn)
42
42
  return
43
43
  end
44
44
  unless File.size(wav.path) > 0
45
- STDERR.puts "ASR: #{executable} produced no output."
45
+ chat.feedback("ASR: #{executable} produced no output.", type: :warn)
46
46
  return
47
47
  end
48
48
 
@@ -61,7 +61,7 @@ module OllamaChat
61
61
  result = JSON.parse(response.body) rescue nil
62
62
  end
63
63
  chat.log(:error, e, data: { status:, result: })
64
- STDERR.puts "ASR transcription failed: #{e.message}"
64
+ chat.feedback("ASR transcription failed: #{e.message}", type: :warn)
65
65
  nil
66
66
  end
67
67
 
@@ -34,6 +34,7 @@ require 'zlib'
34
34
  class OllamaChat::Chat
35
35
  include Tins::GO
36
36
  include Term::ANSIColor
37
+ include OllamaChat::Feedback
37
38
  include OllamaChat::TokenEstimator
38
39
  include OllamaChat::HTTPHandling
39
40
  include OllamaChat::Commands
@@ -64,6 +65,7 @@ class OllamaChat::Chat
64
65
  include OllamaChat::ConfigHandling
65
66
  include OllamaChat::PersonaeManagement
66
67
  include OllamaChat::SessionManagement
68
+ include OllamaChat::CollectionSearch
67
69
  include OllamaChat::RAGHandling
68
70
  include OllamaChat::FavouritesManagement
69
71
  include OllamaChat::PromptHandling
@@ -195,7 +197,7 @@ class OllamaChat::Chat
195
197
  abort "Failed to use to model: #{e}"
196
198
  end
197
199
 
198
- STDOUT.puts
200
+ feedback ?\n
199
201
 
200
202
  setup_persona_from_session
201
203
  setup_system_prompt
@@ -203,7 +205,7 @@ class OllamaChat::Chat
203
205
  print_welcome
204
206
  info_session
205
207
 
206
- STDOUT.puts "\nType /help to display the chat help."
208
+ feedback("\nType /help to display the chat help.")
207
209
 
208
210
  interact_with_user
209
211
  end
@@ -312,10 +314,11 @@ class OllamaChat::Chat
312
314
  # instance when a background thread was spawned (so callers can call
313
315
  # `cancel_speaking`), or `nil` for inline playback or when voice is off
314
316
  def speak(text, background: true)
317
+ session or return
315
318
  voice.on? or return
316
- voice = session.current_voice.full? or return
319
+ current_voice = session.current_voice.full? or return
317
320
  voice_handler.method_defined?(:speak) or return
318
- voice_handler.new(chat: self, voice:).speak(text, background:)
321
+ voice_handler.new(chat: self, voice: current_voice).speak(text, background:)
319
322
  end
320
323
 
321
324
  # Returns a human-readable string representation of the Chat object,
@@ -434,19 +437,19 @@ class OllamaChat::Chat
434
437
  case what
435
438
  when 'messages', nil
436
439
  messages.clear
437
- STDOUT.puts "Cleared messages."
440
+ feedback("Cleared messages.", type: :info)
438
441
  when 'links'
439
442
  links.clear
440
- STDOUT.puts "Cleared links."
443
+ feedback("Cleared links.", type: :info)
441
444
  when 'history'
442
445
  clear_history
443
- STDOUT.puts "Cleared history."
446
+ feedback("Cleared history.", type: :info)
444
447
  when 'tags'
445
448
  @documents.clear
446
- STDOUT.puts "Cleared all tags."
449
+ feedback("Cleared all tags.", type: :info)
447
450
  when 'images'
448
451
  messages.clear_images
449
- STDOUT.puts "Cleared all images."
452
+ feedback("Cleared all images.", type: :info)
450
453
  when 'all'
451
454
  if confirm?(
452
455
  prompt: '🔔 Are you sure to clear messages and collection? (y/n) ',
@@ -457,9 +460,9 @@ class OllamaChat::Chat
457
460
  @documents.clear
458
461
  links.clear
459
462
  clear_history
460
- STDOUT.puts "Cleared messages and collection #{bold{collection}}."
463
+ feedback("Cleared messages and collection #{bold{collection}}.", type: :info)
461
464
  else
462
- STDOUT.puts 'Cancelled.'
465
+ feedback("Denied.", type: :denied)
463
466
  end
464
467
  end
465
468
  persona_profile
@@ -517,7 +520,7 @@ class OllamaChat::Chat
517
520
  type = message.type.full?(:to_sym) || :socket_input
518
521
  content = message.content
519
522
  @parse_content = message.parse
520
- STDOUT.puts color(112) { "Received a server socket message. Processing now…" }
523
+ feedback color(112) { "Received a server socket message. Processing now…" }, type: :info
521
524
  else
522
525
  raise
523
526
  end
@@ -551,17 +554,8 @@ class OllamaChat::Chat
551
554
  group_uuid:
552
555
  )
553
556
 
554
- if runtime_info.on?
555
- tool_name = 'runtime_information'
556
- messages << OllamaChat::Message.new(
557
- role: 'user',
558
- tool_name: ,
559
- sender_name: tool_name,
560
- content: dynamic_runtime_information,
561
- images: @images.dup,
562
- group_uuid:
563
- )
564
- end
557
+ runtime_information_inject(group_uuid:)
558
+ trigger_inject(content, group_uuid:)
565
559
  end
566
560
  @images.clear
567
561
  handler = OllamaChat::FollowChat.new(
@@ -619,13 +613,13 @@ class OllamaChat::Chat
619
613
  msg = "Currently lost connection to ollama server and cannot send command."
620
614
  log(:warn, msg, warn: true)
621
615
  rescue Interrupt
622
- STDOUT.puts "Type /quit to quit."
616
+ feedback("Type /quit to quit.")
623
617
  ensure
624
618
  self.server_socket_message = nil
625
619
  end
626
620
  0
627
621
  rescue OllamaChat::OllamaChatQuitError
628
- STDOUT.puts "Goodbye."
622
+ feedback("Goodbye.")
629
623
  rescue ComplexConfig::AttributeMissing, ComplexConfig::ConfigurationSyntaxError => e
630
624
  log(:error, e)
631
625
  fix_config(e)
@@ -718,7 +712,7 @@ class OllamaChat::Chat
718
712
  # @note Progress is reported to STDOUT during processing
719
713
  def add_documents_from_argv(document_list)
720
714
  if document_list.any?(&:empty?)
721
- STDOUT.puts "Clearing collection #{bold{documents.collection}}."
715
+ feedback("Clearing collection #{bold{documents.collection}}.", type: :info)
722
716
  documents.clear
723
717
  document_list.reject!(&:empty?)
724
718
  end
@@ -730,7 +724,7 @@ class OllamaChat::Chat
730
724
  File.expand_path(doc)
731
725
  end
732
726
  end
733
- STDOUT.puts "Collection #{bold{documents.collection}}: Adding #{document_list.size} documents…"
727
+ feedback("Collection #{bold{documents.collection}}: Adding #{document_list.size} documents…", type: :info)
734
728
  count = 1
735
729
  document_list.each_slice(25) do |docs|
736
730
  docs.each do |doc|
@@ -116,11 +116,11 @@ module OllamaChat::Clipboard
116
116
  text = last_message_content(content: false)
117
117
  perform_copy_to_clipboard(text:, edit:)
118
118
  log(:info, "Copied to clipboard", data: { bytes: format_bytes(text.bytesize) }) if text
119
- STDOUT.puts "The last response has been successfully copied to the system clipboard."
119
+ feedback("The last response has been successfully copied to the system clipboard.", type: :info)
120
120
  true
121
121
  rescue OllamaChat::OllamaChatError => e
122
122
  log(:error, e, data: { method: __method__ })
123
- STDERR.puts e.message
123
+ feedback(e.message, type: :warn)
124
124
  end
125
125
 
126
126
  # Pastes content from the system clipboard into the chat.
@@ -137,11 +137,11 @@ module OllamaChat::Clipboard
137
137
  def paste_from_clipboard(edit: false)
138
138
  result = perform_paste_from_clipboard(edit:)
139
139
  log(:info, "Pasted from clipboard", data: { bytes: format_bytes(result.bytesize) }) if result
140
- STDOUT.puts "The clipboard content has been successfully copied to the chat."
140
+ feedback("The clipboard content has been successfully copied to the chat.", type: :info)
141
141
  result
142
142
  rescue OllamaChat::OllamaChatError => e
143
143
  log(:error, e, data: { method: __method__ })
144
- STDERR.puts e.message
144
+ feedback(e.message, type: :warn)
145
145
  end
146
146
 
147
147
  # Read the full text from STDIN and return it.
@@ -0,0 +1,162 @@
1
+ # Shared collection search primitives for document retrieval and LLM
2
+ # reranking.
3
+ #
4
+ # Extracted from {OllamaChat::Tools::SearchKnowledge} so that the
5
+ # `/collection query` command and the trigger mechanism can reuse
6
+ # the same search + rerank pipeline without duplicating logic.
7
+ module OllamaChat::CollectionSearch
8
+ include Kramdown::ANSI::Width
9
+
10
+ # Searches the active document collection for records matching
11
+ # the given query.
12
+ #
13
+ # @param query [String] the search query
14
+ # @param tags [Array<String>, nil] tag filter
15
+ # @param text_size [Integer, nil] max snippet size
16
+ # @param text_count [Integer, nil] max number of snippets
17
+ # @param min_similarity [Float, nil] minimum similarity threshold
18
+ #
19
+ # @return [Array<Documentrix::Utils::TagResult>] matching records
20
+ def find_document_records(
21
+ query,
22
+ tags: nil, text_size: nil, text_count: nil, min_similarity: nil
23
+ )
24
+ tags = Documentrix::Utils::Tags.new(
25
+ tags, valid_tag: /\A#*([-\w.\]\[]+)/
26
+ )
27
+ effective_query = query.first(
28
+ config.embedding.model.context_length
29
+ )
30
+ embedding_prompt = config.embedding.model.prompt?
31
+ log(
32
+ :info, 'Embedding search request',
33
+ data: {
34
+ model: config.embedding.model.to_h,
35
+ collection: collection,
36
+ query: effective_query,
37
+ prompt: embedding_prompt,
38
+ }
39
+ )
40
+ records = @documents.find_where(
41
+ effective_query,
42
+ tags:,
43
+ prompt: embedding_prompt,
44
+ text_size:,
45
+ text_count:,
46
+ min_similarity:
47
+ )
48
+ log(
49
+ :info, 'Embedding search response',
50
+ data: {
51
+ collection: collection,
52
+ query: effective_query,
53
+ result_count: records.size,
54
+ top: records.first&.similarity&.round(4),
55
+ }
56
+ )
57
+ records
58
+ end
59
+
60
+ # Reranks a set of records using the LLM with the `rerank` prompt.
61
+ #
62
+ # @param query [String] the original search query
63
+ # @param records [Array<Documentrix::Utils::TagResult>] candidate records
64
+ # @param prompt_name [String, nil] the prompt template to use for
65
+ # reranking; defaults to `'rerank'`
66
+ # @return [Array<Documentrix::Utils::TagResult>] filtered records
67
+ def rerank_records(query, records, prompt_name: nil)
68
+ candidates = records.each_with_index.map { |r, i|
69
+ "[#{i}] #{r.tags_set.to_s(link: false)} #{truncate(r.text.squeeze, length: 300)}"
70
+ }.join("\n")
71
+
72
+ prompt_name ||= 'default'
73
+ rerank_prompt = prompt(prompt_name, context: 'rerank') or
74
+ raise 'missing prompt %s' % prompt_name.inspect
75
+ rerank_prompt = rerank_prompt.to_s
76
+ .named_placeholders_interpolate({ query:, candidates: })
77
+
78
+ begin
79
+ if response = generate(prompt: rerank_prompt).full?
80
+ indices = response.scan(/\d+/).map(&:to_i)
81
+ .select { |i| (0...records.size).include?(i) }
82
+ records = records.values_at(*indices) if indices.any?
83
+ end
84
+ rescue => e
85
+ log(:error, e, data: { context: 'rerank' })
86
+ end
87
+ records
88
+ end
89
+
90
+ # Injects relevant snippets from the session's trigger collection
91
+ # into the conversation as a tool message.
92
+ #
93
+ # Reads the first entry of the session's `trigger` hash, performs a
94
+ # vector search in the configured collection, reranks the candidates
95
+ # via the LLM, and appends a `trigger_inject` tool message containing
96
+ # the surviving snippets (text, similarity, tags). The message is
97
+ # grouped with the current user message via `group_uuid`.
98
+ #
99
+ # @param content [String] the parsed user message text used as the
100
+ # search query
101
+ # @param group_uuid [String, nil] the group UUID for message grouping
102
+ # (shared with the user message and runtime-info message)
103
+ #
104
+ # @return [self, nil] self if snippets were injected, nil if no
105
+ # trigger is configured, the trigger or embedding is disabled, the
106
+ # collection or prompt is missing, or no snippets survived reranking
107
+ def trigger_inject(content, group_uuid:)
108
+ embedding.on? or return
109
+ trigger = session.trigger&.first or return
110
+ collection, config = trigger
111
+ unless config['enabled']
112
+ log(:info, 'Trigger for collection %s disabled.' % collection)
113
+ return
114
+ end
115
+ unless col = database_collection?(collection)
116
+ log(:error, 'Unknown collection named %s' % collection)
117
+ return
118
+ end
119
+ prompt_name = config['prompt_name']
120
+ unless rerank_prompt = prompt(prompt_name, context: 'rerank').full?(:to_s)
121
+ log(
122
+ :error, 'Unknown rerank prompt named %s' % prompt_name,
123
+ data: { config: }
124
+ )
125
+ return
126
+ end
127
+ records = []
128
+ switch_collection(collection) do
129
+ records = find_document_records(content, text_count: config['text_count'])
130
+ records.empty? and return
131
+ pre_rerank = records.size
132
+ records = rerank_records(content, records, prompt_name:)
133
+ log(:info, 'Trigger: %d/%d passed rerank for %s' % [ records.size, pre_rerank, collection ],
134
+ data: { collection:, prompt_name: })
135
+ records.empty? and return
136
+ end
137
+ snippets = records.map { |record|
138
+ {
139
+ text: record.text,
140
+ similarity: record.similarity.to_f,
141
+ tags: record.tags_set.to_s(link: false),
142
+ }
143
+ }
144
+ message_content = {
145
+ prompt: prompt('snippets_trigger').to_s,
146
+ collection: {
147
+ name: collection,
148
+ description: col.description&.to_s,
149
+ },
150
+ snippets:
151
+ }.to_json
152
+ tool_name = 'trigger_inject'
153
+ messages << OllamaChat::Message.new(
154
+ role: 'user',
155
+ tool_name: ,
156
+ sender_name: tool_name,
157
+ content: message_content,
158
+ group_uuid:
159
+ )
160
+ self
161
+ end
162
+ end