ollama_chat 0.0.125 → 0.0.126

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +90 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/commands.rb +23 -16
  5. data/lib/ollama_chat/compaction.rb +1 -1
  6. data/lib/ollama_chat/follow_chat.rb +1 -1
  7. data/lib/ollama_chat/information.rb +6 -20
  8. data/lib/ollama_chat/ollama_chat_config/default_config.yml +5 -3
  9. data/lib/ollama_chat/pager.rb +5 -2
  10. data/lib/ollama_chat/rag_handling.rb +211 -22
  11. data/lib/ollama_chat/session_management.rb +24 -4
  12. data/lib/ollama_chat/tool_calling.rb +3 -16
  13. data/lib/ollama_chat/tools/browse.rb +0 -1
  14. data/lib/ollama_chat/tools/compute_bmi.rb +1 -2
  15. data/lib/ollama_chat/tools/concern.rb +26 -0
  16. data/lib/ollama_chat/tools/copy_to_clipboard.rb +1 -3
  17. data/lib/ollama_chat/tools/delete_file.rb +4 -3
  18. data/lib/ollama_chat/tools/directory_structure.rb +0 -1
  19. data/lib/ollama_chat/tools/eval_ruby.rb +0 -1
  20. data/lib/ollama_chat/tools/execute_grep.rb +0 -2
  21. data/lib/ollama_chat/tools/execute_jira_twg.rb +0 -2
  22. data/lib/ollama_chat/tools/execute_ri.rb +0 -1
  23. data/lib/ollama_chat/tools/execute_shell.rb +4 -6
  24. data/lib/ollama_chat/tools/file_context.rb +0 -2
  25. data/lib/ollama_chat/tools/forget.rb +20 -8
  26. data/lib/ollama_chat/tools/gem_path_lookup.rb +0 -1
  27. data/lib/ollama_chat/tools/generate_image.rb +6 -8
  28. data/lib/ollama_chat/tools/generate_password.rb +0 -2
  29. data/lib/ollama_chat/tools/get_current_weather.rb +2 -4
  30. data/lib/ollama_chat/tools/get_cve.rb +0 -2
  31. data/lib/ollama_chat/tools/get_endoflife.rb +0 -2
  32. data/lib/ollama_chat/tools/get_ghr.rb +2 -4
  33. data/lib/ollama_chat/tools/get_jira_issue.rb +0 -2
  34. data/lib/ollama_chat/tools/get_location.rb +0 -2
  35. data/lib/ollama_chat/tools/get_rfc.rb +0 -2
  36. data/lib/ollama_chat/tools/get_time.rb +2 -3
  37. data/lib/ollama_chat/tools/get_url.rb +0 -2
  38. data/lib/ollama_chat/tools/lookup_group.rb +0 -2
  39. data/lib/ollama_chat/tools/memorize.rb +38 -13
  40. data/lib/ollama_chat/tools/move_file.rb +8 -4
  41. data/lib/ollama_chat/tools/open_file_in_editor.rb +0 -2
  42. data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -4
  43. data/lib/ollama_chat/tools/paste_into_editor.rb +3 -5
  44. data/lib/ollama_chat/tools/patch_file.rb +10 -7
  45. data/lib/ollama_chat/tools/read_file.rb +3 -5
  46. data/lib/ollama_chat/tools/resolve_tag.rb +1 -2
  47. data/lib/ollama_chat/tools/roll_dice.rb +0 -2
  48. data/lib/ollama_chat/tools/run_tests.rb +2 -2
  49. data/lib/ollama_chat/tools/search_knowledge.rb +4 -3
  50. data/lib/ollama_chat/tools/search_web.rb +0 -3
  51. data/lib/ollama_chat/tools/write_file.rb +4 -3
  52. data/lib/ollama_chat/tools.rb +50 -4
  53. data/lib/ollama_chat/version.rb +1 -1
  54. data/ollama_chat.gemspec +4 -4
  55. data/spec/ollama_chat/commands_spec.rb +23 -9
  56. data/spec/ollama_chat/rag_handling_spec.rb +191 -18
  57. data/spec/ollama_chat/session_management_spec.rb +6 -2
  58. data/spec/ollama_chat/tool_calling_spec.rb +0 -11
  59. data/spec/ollama_chat/tools/browse_spec.rb +11 -7
  60. data/spec/ollama_chat/tools/compute_bmi_spec.rb +8 -8
  61. data/spec/ollama_chat/tools/concern_spec.rb +7 -1
  62. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -8
  63. data/spec/ollama_chat/tools/delete_file_spec.rb +7 -7
  64. data/spec/ollama_chat/tools/directory_structure_spec.rb +7 -7
  65. data/spec/ollama_chat/tools/execute_grep_spec.rb +10 -10
  66. data/spec/ollama_chat/tools/execute_jira_twg_spec.rb +8 -8
  67. data/spec/ollama_chat/tools/execute_ri_spec.rb +6 -6
  68. data/spec/ollama_chat/tools/execute_shell_spec.rb +1 -1
  69. data/spec/ollama_chat/tools/file_context_spec.rb +4 -4
  70. data/spec/ollama_chat/tools/forget_spec.rb +25 -15
  71. data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +5 -5
  72. data/spec/ollama_chat/tools/generate_image_spec.rb +10 -10
  73. data/spec/ollama_chat/tools/generate_password_spec.rb +8 -8
  74. data/spec/ollama_chat/tools/get_current_weather_spec.rb +6 -6
  75. data/spec/ollama_chat/tools/get_cve_spec.rb +5 -5
  76. data/spec/ollama_chat/tools/get_endoflife_spec.rb +5 -5
  77. data/spec/ollama_chat/tools/get_ghr_spec.rb +11 -11
  78. data/spec/ollama_chat/tools/get_jira_issue_spec.rb +6 -6
  79. data/spec/ollama_chat/tools/get_location_spec.rb +6 -6
  80. data/spec/ollama_chat/tools/get_rfc_spec.rb +5 -5
  81. data/spec/ollama_chat/tools/get_time_spec.rb +4 -4
  82. data/spec/ollama_chat/tools/get_url_spec.rb +17 -17
  83. data/spec/ollama_chat/tools/lookup_group_spec.rb +5 -5
  84. data/spec/ollama_chat/tools/memorize_spec.rb +67 -11
  85. data/spec/ollama_chat/tools/move_file_spec.rb +8 -8
  86. data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -6
  87. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +7 -7
  88. data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -6
  89. data/spec/ollama_chat/tools/patch_file_spec.rb +9 -9
  90. data/spec/ollama_chat/tools/read_file_spec.rb +12 -12
  91. data/spec/ollama_chat/tools/resolve_tag_spec.rb +6 -6
  92. data/spec/ollama_chat/tools/roll_dice_spec.rb +6 -6
  93. data/spec/ollama_chat/tools/run_tests_spec.rb +13 -13
  94. data/spec/ollama_chat/tools/search_knowledge_spec.rb +17 -17
  95. data/spec/ollama_chat/tools/search_web_spec.rb +6 -6
  96. data/spec/ollama_chat/tools/write_file_spec.rb +15 -15
  97. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3de94c682281a4667e89feee589883bd33acf082165f37ce443072117a773167
4
- data.tar.gz: 94cccbb92af48198a70879cf1028a0aebfe0cda49e25ccd82352cb35d9054bf7
3
+ metadata.gz: 93612bbed949bff6bad6cf89f581b324165c0aa672f6f92b675b63a6a2dce7e0
4
+ data.tar.gz: 79aac9f7dada8df87035af5bdfd2913d16012af44a781b7a20c44a457163b313
5
5
  SHA512:
6
- metadata.gz: d3bad30e34ad309fe6c2f2263802af2778eded9459affc6c6713c6383f58706fe54411e7a4b6f087c487718d0a6ea8e07e90dfed22ca1a64b563d207ce61287e
7
- data.tar.gz: 8f9befaa4a6a519b8f7b4424f0d16380663b863e039f08142fc667b01ecb83aa989b9e635ea5373b22191c7ba8f568e97dd210d732d1d6d5099dd3e5cffb74ee
6
+ metadata.gz: 2eb715b50338ddb686e6aab7debd264591e1018216adef57146237a862a5f37f217a31bb481a724b4bb8fcc99144c1a40679e3048c64cb88f0eaaabd546661fc
7
+ data.tar.gz: 8fa30b91ba125939cd506078bcaf6c9549bb43aa6323f9186e0acae218c7814fd8add54aa75b39f907d4affc78889c52a45b2479ec57c4218d76e61e67843ed3
data/CHANGES.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-05 v0.0.126
4
+
5
+ * **Tool Registry Refactor**: Refactored the tool registry to store tool
6
+ classes instead of pre-built instances.
7
+ * `registered_class(name)` now returns the class for `summary_template`
8
+ access.
9
+ * `registered_tool(name, chat:)` instantiates a fresh instance bound to
10
+ the current session.
11
+ * `Concern#initialize(chat)` injects the chat reference; all tools now
12
+ access config via `self.chat` instead of `opts[:chat]`.
13
+ * Helper methods in `execute_shell`, `generate_image`,
14
+ `get_current_weather`, `get_ghr`, and `patch_file` now use `self.chat`
15
+ directly.
16
+ * Migrated all tool specs to use `described_class.new(chat)` and
17
+ `.execute(tool_call)`.
18
+
19
+ * **Path Allowlists**: Moved path allowlists into tool parameter
20
+ descriptions.
21
+ * Added `allowed_paths` to `tools/concern.rb` to return expanded,
22
+ existing paths for the calling tool's `allowed` config.
23
+ * Removed `tool_paths_allowed` from `tool_calling.rb` and its spec.
24
+ * Injected `allowed_paths.map(&:inspect).join(', ')` into the `path`,
25
+ `source`, and `destination` parameter descriptions of `delete_file`,
26
+ `move_file`, `patch_file`, `read_file`, and `write_file`.
27
+
28
+ * **RAG Handling & Runtime Info**: Relocated `collection_descriptions` from
29
+ `information.rb` to `rag_handling.rb`.
30
+ * Removed `collections` key from `static_runtime_information_values` and
31
+ `default_config.yml`.
32
+ * Migrated `static_runtime_information` and `dynamic_runtime_information`
33
+ from `String#%` to `named_placeholders_interpolate`.
34
+
35
+ * **Collection Commands**: Reworked collection clearing into three distinct
36
+ commands.
37
+ * Split monolithic `clear_collection` into `clear_whole_collection`,
38
+ `clear_collection_tags`, and `clear_collection_sources`.
39
+ * `clear_collection_tags` implements a two-level tag-to-source browser.
40
+ * `clear_collection_sources` implements a source-to-tags browser.
41
+ * Added `use_pager(force:)` keyword in `pager.rb` to bypass size checks
42
+ using `Float::INFINITY`.
43
+ * Added `Kramdown::ANSI::Width.wrap(..., percentage: 90)` to pager views
44
+ for terminal-width wrapping.
45
+ * Bumped `documentrix` minimum to **0.8.0** in `Rakefile` and
46
+ `ollama_chat.gemspec` for the new `records(tags:, sources:)` API.
47
+ * Extended `/collection` regexp and completion for `clear sources`.
48
+
49
+ * **Memory Tools**: Enhanced memory management tools.
50
+ * `memorize` now accepts an optional `update` kwarg (ISO timestamp tag)
51
+ to replace an existing memory entry, returning `replaced_timestamp` and
52
+ `replaced_text`.
53
+ * `forget` now returns `forgotten_text` in the JSON response and includes
54
+ an 80-character snippet in the human-readable `message`.
55
+ * Renamed `set_current_collection` to `set_documents_collection` in
56
+ `rag_handling.rb`, `session_management.rb`, and specs.
57
+ * Made `current_collection` public in `RAGHandling`.
58
+ * Enriched `search_knowledge` tool `collection` param with allowed
59
+ collections from `collection_descriptions` and current default.
60
+
61
+ * **Session Management**:
62
+ * Derive memory trigger from persona at session apply by adding a
63
+ `trigger` block to `default_config.yml`.
64
+ * Extracted `create_memory_collection` into `RAGHandling` as the single
65
+ canonical entry point.
66
+ * In `SessionManagement#session_apply`, auto-derive the session trigger
67
+ from `initial_persona_name` and `config.trigger` when no explicit
68
+ trigger is set.
69
+ * Advertise `C-u` random shortcut in the session prompt to clear prefill
70
+ and obtain a random session name.
71
+
72
+ * **Commands & UX**:
73
+ * Added `-r` flag to `/compose` to quote the last assistant reply as an
74
+ `edit_text` pre-fill.
75
+ * Removed the `confirm?` prompt before `compact_with_retry` in
76
+ `/conversation compact`; compaction now runs immediately.
77
+ * Split `/collection clear` into `clear tags` subcommand and bare `clear`
78
+ routing to `clear_whole_collection`.
79
+
80
+ * **Tool Descriptions & Logging**:
81
+ * Made `paste_into_editor` description imperative.
82
+ * Documented bottom-up edit order in `patch_file` to prevent models from
83
+ incorrectly compensating for line shifts.
84
+ * Log full `edits` array in `patch_file` tool info log payload instead of
85
+ just `edits_count`.
86
+ * Strip ANSI codes from `run_tests` captured output using
87
+ `OllamaChat::Utils::StripANSI`.
88
+
89
+ * **Compatibility & Fixes**:
90
+ * Include `persona_name` in the `default` system prompt template in
91
+ `default_config.yml`.
92
+
3
93
  ## 2026-10-03 v0.0.125
4
94
 
5
95
  ### Added
data/Rakefile CHANGED
@@ -40,7 +40,7 @@ GemHadar do
40
40
 
41
41
  dependency 'excon', '~> 1.0'
42
42
  dependency 'ollama-ruby', '~> 1.23'
43
- dependency 'documentrix', '>= 0.7.0'
43
+ dependency 'documentrix', '>= 0.8.0'
44
44
  dependency 'unix_socks', '>= 0.4'
45
45
  dependency 'rss', '~> 0.3'
46
46
  dependency 'term-ansicolor', '~> 1.11'
@@ -427,15 +427,7 @@ module OllamaChat::Commands
427
427
  feedback("Denied.", type: :denied)
428
428
  end
429
429
  when 'compact'
430
- if confirm?(
431
- prompt: '🔔 Compact conversation? Old messages will be summarized. (y/n) ',
432
- yes: /\Ay/i
433
- )
434
- then
435
- compact_with_retry
436
- else
437
- feedback("Denied.", type: :denied)
438
- end
430
+ compact_with_retry
439
431
  when 'summarize'
440
432
  opts = go_command('s', opts)
441
433
  summarize_conversation(sentence: opts[?s])
@@ -648,12 +640,12 @@ module OllamaChat::Commands
648
640
 
649
641
  command(
650
642
  name: :collection,
651
- regexp: %r(^/collection(?:\s+(change|clear|list|rename|update(?: all)?|new|edit|delete|query))?((?:\s+-[er])*)?$),
652
- complete: [ 'collection', %w[ change clear list rename update update\ all new edit delete query ] ],
643
+ regexp: %r(^/collection(?:\s+(change|clear(?: (?:tags|sources))?|list|rename|update(?: all)?|new|edit|delete|query))?((?:\s+-[er])*)?$),
644
+ complete: [ 'collection', %w[ change clear\ tags clear\ sources clear list rename update update\ all new edit delete query ] ],
653
645
  optional: true,
654
646
  help: <<~EOT
655
647
  📚 Manage RAG collections:
656
- - change/clear/list/rename
648
+ - change/clear/clear tags/clear sources/list/rename
657
649
  - update: Re-index modified docs
658
650
  - update all: Re-index all collections
659
651
  - new: Interactively create a new collection
@@ -665,7 +657,11 @@ module OllamaChat::Commands
665
657
  ) do |subcommand, opts|
666
658
  case subcommand
667
659
  when 'clear'
668
- clear_collection
660
+ clear_whole_collection
661
+ when 'clear tags'
662
+ clear_collection_tags
663
+ when 'clear sources'
664
+ clear_collection_sources
669
665
  when 'change'
670
666
  choose_collection(collection)
671
667
  when 'list'
@@ -829,12 +825,23 @@ module OllamaChat::Commands
829
825
 
830
826
  command(
831
827
  name: :compose,
832
- regexp: %r(^/compose$),
828
+ regexp: %r(^/compose(\s+-r)?\s*$),
829
+ options: '[-r]',
833
830
  help: <<~EOT
834
831
  \u270D Compose message in external editor
832
+ Options: -r pre-fill with quoted last assistant reply
835
833
  EOT
836
- ) do
837
- edit_text.full? or :next
834
+ ) do |opts|
835
+ opts = go_command('r', opts)
836
+ prefill = if opts[?r]
837
+ if msg = messages.find_last(content: true) { !_1.tool? && _1.role == 'assistant' }
838
+ msg.content.to_s.lines.map { "> #{_1}" }.join + "\n\n"
839
+ else
840
+ feedback('No assistant message to quote.', type: :warn)
841
+ next :next
842
+ end
843
+ end
844
+ edit_text(prefill).full? or :next
838
845
  end
839
846
 
840
847
  command(
@@ -21,7 +21,7 @@ module OllamaChat::Compaction
21
21
  # @return [String] a short natural-language description of the call.
22
22
  def tool_summary_line(tool_name, result)
23
23
  default_summary = "was called."
24
- klass = OllamaChat::Tools.registered[tool_name.to_s]&.class
24
+ klass = OllamaChat::Tools.registered_class(tool_name)
25
25
  klass&.summary_template(result:) || default_summary
26
26
  rescue StandardError
27
27
  default_summary
@@ -180,7 +180,7 @@ class OllamaChat::FollowChat
180
180
  feedback(
181
181
  ("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ])
182
182
  )
183
- result = OllamaChat::Tools.registered[name].execute(tool_call, chat:)
183
+ result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call)
184
184
  chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
185
185
  end
186
186
 
@@ -327,22 +327,6 @@ module OllamaChat::Information
327
327
  config.infobar.message.to_h
328
328
  end
329
329
 
330
- # Retrieves a hash of collection names and their descriptions from the
331
- # database.
332
- #
333
- # This is used to provide context to the AI model about available RAG
334
- # collections.
335
- #
336
- # @return [Hash{String => String}] a hash mapping collection names to their
337
- # descriptions
338
- def collection_descriptions
339
- cols = models::Collection.where(enabled: true)
340
- .select(:name, :description).order(:name)
341
- cols.each_with_object({}) do |c, hash|
342
- hash[c.name] = c.description
343
- end
344
- end
345
-
346
330
  # Generates a hash containing static runtime information.
347
331
  #
348
332
  # This method collects session-level constants including the user,
@@ -353,11 +337,9 @@ module OllamaChat::Information
353
337
  def static_runtime_information_values
354
338
  {
355
339
  client: ,
356
- collections: JSON.pretty_generate(collection_descriptions),
357
340
  current_directory: Pathname.pwd.expand_path.to_path,
358
341
  languages: config.languages * ', ',
359
342
  location: location.on?.full? { location_description } || 'n/a',
360
- tool_paths_allowed: JSON.pretty_generate(tool_paths_allowed),
361
343
  user: ,
362
344
  }
363
345
  end
@@ -369,7 +351,9 @@ module OllamaChat::Information
369
351
  #
370
352
  # @return [String] a formatted static runtime information string.
371
353
  def static_runtime_information
372
- prompt(:static_runtime_info).to_s % static_runtime_information_values
354
+ prompt(:static_runtime_info).to_s.named_placeholders_interpolate(
355
+ static_runtime_information_values
356
+ )
373
357
  end
374
358
 
375
359
  # Returns a formatted string representing the estimated text length of
@@ -423,7 +407,9 @@ module OllamaChat::Information
423
407
  #
424
408
  # @return [String] the formatted dynamic runtime information string.
425
409
  def dynamic_runtime_information
426
- prompt(:dynamic_runtime_info).to_s % dynamic_runtime_information_values
410
+ prompt(:dynamic_runtime_info).to_s.named_placeholders_interpolate(
411
+ dynamic_runtime_information_values
412
+ )
427
413
  end
428
414
 
429
415
  # Injects the current dynamic runtime information (the "heartbeat")
@@ -56,8 +56,6 @@ prompts:
56
56
  - Location: %{location}
57
57
  - Current directory is "%{current_directory}"
58
58
  - Client is %{client}.
59
- - These tools have access to these allowed paths: %{tool_paths_allowed}
60
- - Search in these collections with `search_knowledge`: %{collections}
61
59
  dynamic_runtime_info: |
62
60
  There is usually no reason to mention this information to the user unless
63
61
  asked about it.
@@ -424,7 +422,7 @@ prompts:
424
422
  To see the full output of any tool call above, use lookup_group
425
423
  with the group UUID in parentheses.
426
424
  system:
427
- default: <%= OC::OLLAMA::CHAT::SYSTEM || "%{persona}\n\n%{runtime_info}".inspect %>
425
+ default: <%= OC::OLLAMA::CHAT::SYSTEM || "`persona_name`: %{persona_name}\n\n%{persona}\n\n%{runtime_info}".inspect %>
428
426
  persona: |
429
427
  `persona_name`: %{persona_name}
430
428
 
@@ -497,6 +495,10 @@ document_policy: importing
497
495
  persona:
498
496
  # Basename (without .md) of the default persona file in the personae directory.
499
497
  default_name: personal_assistant
498
+ trigger:
499
+ enabled: true
500
+ prompt_name: memory_rerank
501
+ text_count: 3
500
502
  parsing:
501
503
  directory_structure_max_depth: 1
502
504
  think:
@@ -12,14 +12,17 @@ module OllamaChat::Pager
12
12
  # If the output would exceed the terminal's line capacity, it pipes the content
13
13
  # through an appropriate pager command (like 'less' or 'more').
14
14
  #
15
+ # @param force [Boolean] bypass the line-count threshold and always
16
+ # invoke the pager, regardless of output length.
15
17
  # @yield A block that yields an IO object to write output to
16
18
  # @yieldparam [IO] the IO object to write to
17
- def use_pager
19
+ def use_pager(force: false)
18
20
  command = determine_pager_command
19
21
  output_buffer = StringIO.new
20
22
  yield output_buffer
21
23
  buffer = output_buffer.string
22
- Kramdown::ANSI::Pager.pager(command:, lines: buffer.count(?\n)) do |output|
24
+ lines = force ? Float::INFINITY : buffer.count(?\n)
25
+ Kramdown::ANSI::Pager.pager(command:, lines:) do |output|
23
26
  output.puts buffer
24
27
  end
25
28
  end
@@ -32,7 +32,32 @@ module OllamaChat::RAGHandling
32
32
  models::Collection[name: collection.to_s]
33
33
  end
34
34
 
35
- private
35
+ # Creates (or reuses) the persistent memory collection for a persona.
36
+ #
37
+ # The collection is named "memory-#{persona_name}" and is registered in
38
+ # the local database with an empty pattern list. If a collection with
39
+ # that name already exists, it is returned as-is without modification.
40
+ #
41
+ # This is the single canonical entry point for ensuring a persona's
42
+ # memory collection exists before it is referenced by the trigger
43
+ # mechanism, the `memorize`/`forget` tools, or any other consumer.
44
+ #
45
+ # @param persona_name [String] the persona stem (e.g. "miyu_pairing")
46
+ # whose memory collection should be available.
47
+ # @return [String] the collection name (e.g. "memory-miyu_pairing").
48
+ def create_memory_collection(persona_name)
49
+ collection = "memory-#{persona_name}"
50
+
51
+ unless chat.database_collection?(collection)
52
+ models::Collection.create(
53
+ name: collection,
54
+ description: "Memory for persona #{persona_name}",
55
+ patterns: [],
56
+ )
57
+ end
58
+
59
+ collection
60
+ end
36
61
 
37
62
  # Returns the name of the currently active document collection.
38
63
  #
@@ -41,45 +66,209 @@ module OllamaChat::RAGHandling
41
66
  @documents.collection
42
67
  end
43
68
 
44
- # Clears documents from the collection through an interactive user interface.
69
+ # Retrieves a hash of collection names and their descriptions from the
70
+ # database.
45
71
  #
46
- # This method allows users to selectively clear documents by choosing
47
- # specific tags from the current collection or to clear all documents in the
48
- # collection. It provides a loop for multiple deletions until the user exits
49
- # or completes a clear operation.
50
- def clear_collection
72
+ # This is used to provide context to the AI model about available RAG
73
+ # collections.
74
+ #
75
+ # @return [Hash{String => String}] a hash mapping collection names to their
76
+ # descriptions
77
+ def collection_descriptions
78
+ cols = models::Collection.where(enabled: true)
79
+ .select(:name, :description).order(:name)
80
+ cols.each_with_object({}) do |c, hash|
81
+ hash[c.name] = c.description
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # The clear_whole_collection method confirms user intent to delete the entire
88
+ # collection, then clears all documents and logs the action, returning self
89
+ # on success.
90
+ #
91
+ # @return [ nil, OllamaChat::Chat ] when the user declines the confirmation
92
+ # prompt, self otherwise.
93
+ def clear_whole_collection
94
+ if confirm?(prompt: '🔔 Really delete the whole collection? Are you sure? (y/n) ', yes: /\Ay/i)
95
+ @documents.clear
96
+ log(:info, "Collection cleared", data: { collection: })
97
+ feedback("Cleared collection #{bold{collection}}.", type: :info)
98
+ self
99
+ else
100
+ feedback('Denied.', type: :denied)
101
+ nil
102
+ end
103
+ end
104
+
105
+ # Clears documents from the collection through an interactive
106
+ # two-level tag browser.
107
+ #
108
+ # Level 1: choose a tag from the collection.
109
+ # Level 2: inspect records grouped by source, with preview (v) and
110
+ # surgical clear (c) options.
111
+ def clear_collection_tags
51
112
  choose_with_state do
52
113
  loop do
53
- tags = @documents.tags.to_a.unshift('[ALL]').unshift('[EXIT]')
114
+ display = @documents.tags.map(&:to_s).sort.unshift('[EXIT]')
54
115
  tag = choose_entry(
55
- tags,
56
- prompt: 'What obsolete records are to be excised from the annals? %s'
116
+ display,
117
+ prompt: 'Which tag to inspect? %s'
57
118
  )
58
119
  case tag
120
+ when nil, '[EXIT]'
121
+ feedback('Exiting chooser.')
122
+ break
123
+ else
124
+ records = @documents.records(tags: tag)
125
+ groups = records.group_by { |r| r.source.full? || '(nil)' }
126
+
127
+ if groups.size == 1 && groups.values.first.size == 1
128
+ record = groups.values.first.first
129
+ source = record.source
130
+ snippet = record.text.strip[0, 72].inspect
131
+ label = source ? "#{source} #{snippet}" : snippet
132
+ feedback(label, type: :info)
133
+ view_tag_records(tag, [record])
134
+ ask?(prompt: " [c]lear / other to go back: ") =~ /\Ac/i or next
135
+ clear_tag_records(tag, source)
136
+ else
137
+ group_lines = []
138
+ group_map = {}
139
+ groups.sort_by { |src, _| src }.each do |src, recs|
140
+ if recs.size == 1
141
+ snippet = recs.first.text.strip[0, 72].inspect
142
+ line = src == '(nil)' ? snippet : "#{src} #{snippet}"
143
+ else
144
+ line = "#{src} (#{recs.size} records)"
145
+ end
146
+ group_lines << line
147
+ group_map[line] = [src, recs]
148
+ end
149
+ group_lines.unshift('[back]')
150
+ chosen = choose_entry(
151
+ group_lines,
152
+ prompt: "Tag #{tag.to_s.gsub(?%, '%%')} — which source group? %s"
153
+ )
154
+ next if chosen.nil? || chosen == '[back]'
155
+ src, recs = group_map[chosen]
156
+ action = ask?(prompt: " [v]iew / [c]lear / other to go back: ")
157
+ case action
158
+ when /\Av/i
159
+ view_tag_records(tag, recs)
160
+ when /\Ac/i
161
+ clear_tag_records(tag, src == '(nil)' ? nil : src)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # Renders tag-filtered records in the pager.
170
+ def view_tag_records(tag, records)
171
+ use_pager(force: true) do |output|
172
+ output.puts "#{bold{tag}} (#{records.size} records)"
173
+ output.puts
174
+ records.each do |record|
175
+ output.puts kramdown_ansi_parse(<<~EOT)
176
+ **#{record.source}**
177
+ ```
178
+ #{Kramdown::ANSI::Width.wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
179
+ ```
180
+ ---
181
+ EOT
182
+ end
183
+ end
184
+ end
185
+
186
+ # Clears records for a tag, using source_remove when a source is
187
+ # present, or clear(tags:) for memory entries (no source).
188
+ def clear_tag_records(tag, source)
189
+ if source
190
+ @documents.source_remove(source)
191
+ else
192
+ @documents.clear(tags: [ tag ])
193
+ end
194
+ log(:info, 'Tag cleared from collection',
195
+ data: { collection:, tag:, source: })
196
+ feedback(
197
+ "Cleared tag #{tag} from collection #{bold{collection}}.",
198
+ type: :info
199
+ )
200
+ end
201
+
202
+ # Removes source documents from the collection through an interactive
203
+ # user interface.
204
+ #
205
+ # Each source is displayed with its associated tags for context.
206
+ # Users can preview a source's records in a pager before confirming
207
+ # removal.
208
+ def clear_collection_sources
209
+ choose_with_state do
210
+ loop do
211
+ sources = Set.new
212
+ source_tags = Hash.new { |h, k| h[k] = Set.new }
213
+ @documents.records.each do |record|
214
+ source = record.source.full? or next
215
+ sources.add(source)
216
+ source_tags[source].merge(record.tags)
217
+ end
218
+ display_map = {}
219
+ sources.each do |s|
220
+ tags = source_tags[s].map(&:to_s)
221
+ line = tags.any? ? "#{s} [#{tags.join(', ')}]" : s
222
+ display_map[line] = s
223
+ end
224
+ display = display_map.keys
225
+ display.unshift('[EXIT]')
226
+ chosen = choose_entry(
227
+ display,
228
+ prompt: 'Which source to remove from the collection? %s'
229
+ )
230
+ case chosen
59
231
  when nil, '[EXIT]'
60
232
  feedback("Exiting chooser.")
61
233
  break
62
- when '[ALL]'
63
- if confirm?(prompt: '🔔 Are you sure? (y/n) ', yes: /\Ay/i)
64
- @documents.clear
65
- log(:info, "Collection cleared", data: { collection: })
66
- feedback("Cleared collection #{bold{collection}}.", type: :info)
67
- break
234
+ else
235
+ source = display_map[chosen]
236
+ action = ask?(prompt: " [v]iew / [c]lear / other to go back: ")
237
+ case action
238
+ when /\Av/i
239
+ records = @documents.records(sources: [ source ])
240
+ use_pager do |output|
241
+ output.puts "#{bold{source}} (#{records.size} records)"
242
+ output.puts
243
+ records.each do |record|
244
+ output.puts kramdown_ansi_parse(<<~EOT)
245
+ **[#{record.tags.join(', ')}]**
246
+ ```
247
+ #{Kramdown::ANSI::Width.wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
248
+ ```
249
+ ---
250
+ EOT
251
+ end
252
+ end
253
+ when /\Ac/i
254
+ @documents.source_remove(source)
255
+ log(:info, "Source removed from collection",
256
+ data: { collection:, source: })
257
+ feedback(
258
+ "Removed source #{source} from collection #{bold{collection}}.",
259
+ type: :info
260
+ )
68
261
  end
69
- when /./
70
- @documents.clear(tags: [ tag ])
71
- log(:info, "Tag cleared from collection", data: { collection:, tag: })
72
- feedback("Cleared tag #{tag} from collection #{bold{collection}}.", type: :info)
73
262
  end
74
263
  end
75
264
  end
76
265
  end
77
266
 
78
- # Sets the current document collection.
267
+ # Sets the documents collection to collection.
79
268
  #
80
269
  # @param collection [String, Symbol] the name of the collection to set
81
270
  # @return [String, Symbol] the newly set collection name
82
- def set_current_collection(collection)
271
+ def set_documents_collection(collection)
83
272
  @documents.collection = collection
84
273
  end
85
274
 
@@ -163,7 +163,7 @@ module OllamaChat::SessionManagement
163
163
  session_name = nil
164
164
  loop do
165
165
  session_name = ask?(
166
- prompt: "❓ Enter new session name #{action}, C-c ⇒ cancel: ",
166
+ prompt: "❓ Enter new session name #{action}, C-u ⇒ random, C-c ⇒ cancel: ",
167
167
  prefill: default_name
168
168
  )
169
169
  if session_name.nil?
@@ -264,7 +264,26 @@ module OllamaChat::SessionManagement
264
264
  end
265
265
  end
266
266
 
267
+ # Applies the loaded session to the chat context.
268
+ #
269
+ # Performs the post-lock setup sequence:
270
+ # 1. Updates the session's working directory to `Dir.pwd`.
271
+ # 2. Initializes the in-memory message history from the session
272
+ # payload.
273
+ # 3. Repairs any orphaned group UUIDs in the message list.
274
+ # 4. Ensures the persistent memory collection for the session's
275
+ # persona exists in the database (no-op if already present).
276
+ #
277
+ # Called from {#setup_session} after the session has been locked.
278
+ #
279
+ # @return [OllamaChat::Database::Models::Session] the applied session
280
+ # instance.
267
281
  def session_apply
282
+ initial_persona_name.full? do |persona_name|
283
+ collection = create_memory_collection(persona_name)
284
+ session.trigger.present? or
285
+ session.trigger = { collection => chat.config.trigger.to_h }
286
+ end
268
287
  session.update(working_directory: Dir.pwd)
269
288
  init_history
270
289
  repair_group_uuids
@@ -503,6 +522,7 @@ module OllamaChat::SessionManagement
503
522
  existing = session.trigger.to_h
504
523
 
505
524
  collection = nil
525
+
506
526
  choose_with_state do
507
527
  # Prefill with the first configured collection as a starting
508
528
  # point. Fine for the single-trigger case; with multiple
@@ -630,11 +650,11 @@ module OllamaChat::SessionManagement
630
650
  if session.lock?(chat: self)
631
651
  messages.read_conversation_jsonl(session.messages.to_s)
632
652
  if current_collection = session.current_collection.full? and
633
- database_collection?(current_collection)
653
+ database_collection?(current_collection)
634
654
  then
635
- set_current_collection(current_collection)
655
+ set_documents_collection(current_collection)
636
656
  else
637
- set_current_collection(:default)
657
+ set_documents_collection(:default)
638
658
  end
639
659
  session.current_model.full? { use_model(_1) }
640
660
  set_default_persona_name(session.default_persona_name.full? || :none)
@@ -55,7 +55,9 @@ 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[_1]&.to_hash }
58
+ enabled_tools.filter_map {
59
+ OllamaChat::Tools.registered_tool(_1, chat: self)&.to_hash
60
+ }
59
61
  end
60
62
 
61
63
  # The default_enabled_tools method returns an array of tool names that are
@@ -181,21 +183,6 @@ module OllamaChat::ToolCalling
181
183
  end
182
184
  end
183
185
 
184
- # The tool_paths_allowed method returns a hash mapping each enabled tool name
185
- # to its list of allowed paths or patterns. @return [Hash] a hash where keys
186
- # are tool names and values are the allowed path lists
187
- def tool_paths_allowed
188
- config.tools.functions.to_h.
189
- select { |name, value| tool_enabled?(name) && value[:allowed].present? }.
190
- sort_by(&:first).
191
- each_with_object({}) do |(name, value), hash|
192
- hash[name.to_s] = value[:allowed].filter_map do
193
- pathname = Pathname.new(_1).expand_path
194
- pathname.exist?.full? { pathname.to_path }
195
- end
196
- end
197
- end
198
-
199
186
  private
200
187
 
201
188
  def handle_tool_call_results?(&block)