ollama_chat 0.0.91 → 0.0.92

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3106d416e537195c72ede3b02c74042b8425f06e53b83982937a1d8134ffe594
4
- data.tar.gz: 24dd954498837dd1c81d437e5e673d90c0e9bd28130ffe9beef1ddd6fc31604a
3
+ metadata.gz: 65c990826623b9e3d8548c69bf01b715dd9597e451a5a4b480e4e09d48249113
4
+ data.tar.gz: bdf07bb49531d867d8be7b2cd8f2c5537867b9a2d297d939f123593952a77507
5
5
  SHA512:
6
- metadata.gz: 91ca2b8c628de67a35ea027ac240d101cad0a283295681f012fc6716bce2e60720f36e4bb493edfff77866cda0210e28368cd7f58526afcc112a8b77ff59a59e
7
- data.tar.gz: ee0fcf6b6575078fc7d3606c68640135e237374ec1673375042a4cfd8174248562431ae9da08e631f1fd4de57e73a3943e5702f1dedc02b8983a0d93165c3331
6
+ metadata.gz: 2ee0222c25f838f0c2436e71b9d9d8aa630fe45c58f7dc592c850e3dbe6b85ec0461883b05d735aab74c664fe8516ef13adecd33c18b0fd5b8571feba6827e65
7
+ data.tar.gz: a7ae3f192f4bead676495ee4053596fd5253e1394e5e2f1a5617f621d201e98d6dddd866c32b9d025a1b22cd3201fdf3be09a5399dab18c462c8bdc4d412b37f
data/CHANGES.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-06-19 v0.0.92
4
+
5
+ ### New Features
6
+
7
+ - **LLM-Based Reranking**: Implemented reranking logic in the retrieval tool
8
+ via a new `rerank_records` method in
9
+ `lib/ollama_chat/tools/retrieve_document_snippets.rb`.
10
+ - Added a new `rerank` prompt template to `default_config.yml`.
11
+ - Refactored the `execute` method to utilize `flat_map` for better handling
12
+ of records without tags.
13
+ - **Prompt Suggestions**: Introduced a `/prompt suggest` subcommand in
14
+ `OllamaChat::Commands`.
15
+ - Implemented `suggest_prompts` and `prepare_conversation_history` within
16
+ `OllamaChat::PromptManagement`.
17
+ - Added `coding_suggest` and `roleplaying_suggest` templates to
18
+ `default_config.yml`.
19
+
20
+ ### Improvements
21
+
22
+ - **CLI User Experience**: Enhanced CLI interactions by replacing generic
23
+ prompts with flavorful, context-aware messages across `ModelHandling`,
24
+ `RAGHandling`, `StateSelectors`, `SystemPromptManagement`,
25
+ `PersonaeManagement`, and `PromptManagement`.
26
+ - **Interaction Logic**: Expanded `choose_prompt` and `choose_system_prompt` to
27
+ support custom prompt messages and updated `choose_entry` signatures.
28
+ - **Observability**: Added info logging for prompt, model, and generation
29
+ details within `Chat#generate`.
30
+ - **Internal API**: Adjusted visibility in `lib/ollama_chat/chat.rb` and
31
+ `lib/ollama_chat/prompt_handling.rb` to allow tools access to core methods.
32
+
33
+ ### Tests
34
+
35
+ - Updated `spec/ollama_chat/tools/retrieve_document_snippets_spec.rb` to cover
36
+ the new `rerank` parameter.
37
+ - Updated `spec/ollama_chat/input_content_spec.rb` and
38
+ `spec/ollama_chat/state_selectors_spec.rb` to align with updated
39
+ `choose_entry` signatures.
40
+
3
41
  ## 2026-06-17 v0.0.91
4
42
 
5
43
  - Updated `lib/ollama_chat/commands.rb` to ensure the result of
@@ -228,8 +228,6 @@ class OllamaChat::Chat
228
228
  @messages.system_name
229
229
  end
230
230
 
231
- private
232
-
233
231
  # The generate method sends a prompt to the Ollama model and returns the
234
232
  # result.
235
233
  #
@@ -238,6 +236,8 @@ class OllamaChat::Chat
238
236
  # @return [ Ollama::Response ] the response from the Ollama model
239
237
  def generate(prompt:)
240
238
  prepare_model(@model)
239
+ msg = "Using prompt #{prompt.inspect} for generation to #{@model.inspect}."
240
+ log(:info, msg)
241
241
  ollama.generate(
242
242
  model: @model,
243
243
  prompt:,
@@ -247,6 +247,8 @@ class OllamaChat::Chat
247
247
  )
248
248
  end
249
249
 
250
+ private
251
+
250
252
  # @return [Module] The module containing the database models.
251
253
  def models
252
254
  OllamaChat::Database::Models
@@ -163,7 +163,10 @@ module OllamaChat::Commands
163
163
  when 'info'
164
164
  info_system_prompt
165
165
  when 'reset'
166
- if prompt = choose_system_prompt
166
+ if prompt = choose_system_prompt(
167
+ prompt: 'Which system law needs to be restored to its origin? '
168
+ )
169
+ then
167
170
  if reset_system_prompt_to_default(prompt.name)
168
171
  STDOUT.puts "Reset system prompt #{bold{prompt.name}} to default."
169
172
  else
@@ -405,12 +408,12 @@ module OllamaChat::Commands
405
408
 
406
409
  command(
407
410
  name: :prompt,
408
- regexp: %r(^/prompt(\s+-e)?(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset))?(?:\s+(\S+))?$),
409
- complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset ] ],
411
+ regexp: %r(^/prompt(\s+-e)?(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|suggest))?(?:\s+(\S+))?$),
412
+ complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset suggest ] ],
410
413
  optional: true,
411
414
  help: <<~EOT,
412
415
  Manage preset prompt templates or prefill the prompt (edit, info, add,
413
- delete, list, duplicate, import, export, reset)
416
+ delete, list, duplicate, import, export, reset, suggest)
414
417
  Options: -e to edit the next prompt instead of prefilling
415
418
  EOT
416
419
  ) do |opts, subcommand, filename|
@@ -432,16 +435,22 @@ module OllamaChat::Commands
432
435
  when 'info'
433
436
  info_prompt
434
437
  when 'reset'
435
- if prompt = choose_prompt(default: true)
438
+ if prompt = choose_prompt(
439
+ default: true,
440
+ prompt: 'Which prompt needs to be restored to its origin? '
441
+ )
442
+ then
436
443
  if reset_prompt_to_default(prompt.name)
437
444
  STDOUT.puts "Reset prompt #{bold{prompt.name}} to default."
438
445
  else
439
446
  STDOUT.puts "No default value found for prompt #{bold{prompt.name}}."
440
447
  end
441
448
  end
449
+ when 'suggest'
450
+ prompt = suggest_prompts and next prompt
442
451
  when nil
443
452
  opts = go_command('e', opts)
444
- if prompt = choose_prompt.full?(&:to_s)
453
+ if prompt = choose_prompt(prompt: 'Which template shall guide the next response? ').full?(&:to_s)
445
454
  if opts[?e]
446
455
  prompt = edit_text(prompt)
447
456
  next prompt
@@ -51,7 +51,7 @@ module OllamaChat::FavouritesManagement
51
51
  return
52
52
  end
53
53
  to_select.unshift('[EXIT]')
54
- case chosen = choose_entry(to_select)
54
+ case chosen = choose_entry(to_select, prompt: 'Select an item to mark as favourite: ')
55
55
  when '[EXIT]', nil
56
56
  STDOUT.puts "Cancelled."
57
57
  return
@@ -77,7 +77,7 @@ module OllamaChat::FavouritesManagement
77
77
  to_select = models::Favourite.where(context: type).map(&:name)
78
78
  to_select = all_things.select { to_select.member?(_1.value) }
79
79
  to_select = [ '[EXIT]' ] + to_select
80
- case chosen = choose_entry(to_select)
80
+ case chosen = choose_entry(to_select, prompt: 'Select a favourite to remove: ')
81
81
  when '[EXIT]', nil
82
82
  STDOUT.puts "Cancelled."
83
83
  return
@@ -55,7 +55,7 @@ module OllamaChat::InputContent
55
55
  files = patterns.flat_map { Pathname.glob(_1) }
56
56
  files = files.reject { chosen&.member?(_1.expand_path) }.select { _1.file? }
57
57
  files.unshift('[EXIT]')
58
- case chosen_file = choose_entry(files)
58
+ case chosen_file = choose_entry(files, prompt: 'Select a file to import: ')
59
59
  when '[EXIT]', nil
60
60
  STDOUT.puts "Exiting chooser."
61
61
  return
@@ -364,7 +364,10 @@ module OllamaChat::ModelHandling
364
364
  if models.size == 1
365
365
  models.first.value
366
366
  elsif cli_model == ''
367
- choose_entry(models)&.value || current_model
367
+ choose_entry(
368
+ models,
369
+ prompt: "Which digital oracle shall we consult?"
370
+ )&.value || current_model
368
371
  else
369
372
  cli_model || current_model
370
373
  end
@@ -24,7 +24,15 @@ infobar:
24
24
  :frames: :braille181
25
25
  :message: ✓
26
26
  prompts:
27
- embed: "This source has been added to or updated in collection \"%{collection}\"."
27
+ embed: This source has been added to or updated in collection "%{collection}".
28
+ rerank: |
29
+ Query: %{query}
30
+ Candidates:
31
+
32
+ %{candidates}
33
+
34
+ Return only the indices of the most relevant snippets as a comma-separated
35
+ list (e.g., '0,2').
28
36
  summarize: |
29
37
  Generate an abstract summary of the content in this document using
30
38
  %{words} words:
@@ -158,7 +166,39 @@ prompts:
158
166
  Response content was
159
167
 
160
168
  %{message_content}
161
- session_title: "Create a title with a length of **less than %{length}** characters for this conversation. Output only the title and nothing else:\n\n%{content}"
169
+ session_title: |
170
+ Create a title with a length of **less than %{length}** characters for this
171
+ conversation. Output only the title and nothing else:
172
+
173
+ %{content}
174
+ suggest_coding: |
175
+ Analyze the conversation history above. Generate exactly 3 distinct,
176
+ concise follow-up prompts that the user might want to ask next to further
177
+ their goal.
178
+
179
+ Criteria:
180
+ 1. One prompt should dive deeper into the current technical implementation.
181
+ 2. One prompt should challenge a potential edge case or optimization.
182
+ 3. One prompt should suggest a logical next step or a broader architectural question.
183
+
184
+ Output only the text of these suggestions, separated by an empty line,
185
+ without any numbering, bullet points, or introductory text.
186
+ suggest_roleplaying: |
187
+ Analyze the conversation history and the current narrative state of this
188
+ roleplay/story. Generate exactly 3 distinct, concise prompts that the
189
+ user could use to respond as their character.
190
+
191
+ Criteria:
192
+ 1. One prompt should be an active choice or bold action that pushes the
193
+ plot forward or creates tension.
194
+ 2. One prompt should focus on character interaction, exploring emotional
195
+ depth, a social reaction, or a subtle nuance in dialogue.
196
+ 3. One prompt should introduce a sudden complication, a risky gamble, or an
197
+ unexpected event. This prompt MUST explicitly include a suggested dice roll
198
+ (e.g., "Roll 1d20") to determine the success or failure of the action.
199
+
200
+ Output only the text of these suggestions, separated by an empty line,
201
+ without any numbering, bullet points, or introductory text.
162
202
  system_prompts:
163
203
  default: <%= OC::OLLAMA::CHAT::SYSTEM || "%{persona}\n\n%{runtime_info}".inspect %>
164
204
  persona: |
@@ -91,7 +91,7 @@ module OllamaChat::PersonaeManagement
91
91
  # @return [String, nil] The name of the persona that was set as default,
92
92
  # or nil if the selection was cancelled or no persona was chosen.
93
93
  def set_default_persona
94
- if persona = choose_persona(none: true)
94
+ if persona = choose_persona(none: true, prompt: 'Who would you like to talk to today? ')
95
95
  set_default_persona_name(persona)
96
96
  end
97
97
  end
@@ -214,7 +214,7 @@ module OllamaChat::PersonaeManagement
214
214
  # @return [String] A JSON object with deletion status on success,
215
215
  # or nil if persona was not selected or deletion was cancelled
216
216
  def delete_persona
217
- if persona = choose_persona
217
+ if persona = choose_persona(prompt: 'Which persona is no longer needed? ')
218
218
  pathname = persona_name_to_pathname(persona)
219
219
  backup_pathname = persona_backup_pathname(persona)
220
220
  if pathname.exist?
@@ -245,7 +245,7 @@ module OllamaChat::PersonaeManagement
245
245
  #
246
246
  # @return [String, nil] persona name or nil if cancelled
247
247
  def edit_persona
248
- if persona = choose_persona
248
+ if persona = choose_persona(prompt: 'Which persona needs some polishing? ')
249
249
  pathname = persona_name_to_pathname(persona)
250
250
  old_content = pathname.read
251
251
  if edit_file(pathname)
@@ -265,7 +265,7 @@ module OllamaChat::PersonaeManagement
265
265
  # @return [String, nil] the filesystem path of the selected persona,
266
266
  # or nil if the selection was cancelled.
267
267
  def select_persona_path
268
- persona = choose_persona or return
268
+ persona = choose_persona(prompt: "Which persona's path do you need? ") or return
269
269
  path = persona_name_to_pathname(persona).to_s
270
270
  perform_copy_to_clipboard(text: path, edit: false)
271
271
  @prefill_prompt = path
@@ -279,7 +279,7 @@ module OllamaChat::PersonaeManagement
279
279
  # location using `File.write`. This ensures a safe copy is preserved before
280
280
  # any modifications are made to the original file.
281
281
  def backup_persona
282
- if persona = choose_persona
282
+ if persona = choose_persona(prompt: 'Which persona should be safely archived? ')
283
283
  pathname = persona_name_to_pathname(persona)
284
284
  old_content = pathname.read
285
285
  backup_pathname = persona_backup_pathname(persona)
@@ -317,7 +317,7 @@ module OllamaChat::PersonaeManagement
317
317
  #
318
318
  # Shows the persona's profile using kramdown formatting with ansi parsing.
319
319
  def info_persona
320
- if persona = choose_persona
320
+ if persona = choose_persona(prompt: 'Who would you like to learn more about? ')
321
321
  description = persona_description(persona) or return
322
322
  use_pager do |output|
323
323
  output.puts kramdown_ansi_parse(description)
@@ -374,9 +374,11 @@ module OllamaChat::PersonaeManagement
374
374
  #
375
375
  # @param chosen [Set, nil] Optional set of already selected personas
376
376
  # @param none [Boolean] whether to include a '[NONE]' option in the list
377
+ # @param prompt [String] the prompt message to display when asking for input
378
+ # (default: 'Select a persona: ')
377
379
  # @return [String, Symbol, nil] The selected persona name, :none, or nil if
378
380
  # user exits
379
- def choose_persona(chosen: nil, none: false)
381
+ def choose_persona(chosen: nil, none: false, prompt: 'Select a persona: ')
380
382
  personae_list = available_personae_names.
381
383
  reject { chosen&.member?(_1) }
382
384
  if personae_list.empty?
@@ -385,7 +387,7 @@ module OllamaChat::PersonaeManagement
385
387
  end
386
388
  personae_list.unshift('[NONE]') if none
387
389
  personae_list.unshift('[EXIT]')
388
- case persona = choose_entry(personae_list)
390
+ case persona = choose_entry(personae_list, prompt:)
389
391
  when '[EXIT]', nil
390
392
  STDOUT.puts "Exiting chooser."
391
393
  return
@@ -403,7 +405,7 @@ module OllamaChat::PersonaeManagement
403
405
  def load_personae
404
406
  chosen = Set[]
405
407
  choose_with_state do
406
- while persona = choose_persona(chosen: chosen)
408
+ while persona = choose_persona(chosen: chosen, prompt: 'Who else should join the conversation? ')
407
409
  persona == :none and next
408
410
  chosen << persona
409
411
  end
@@ -532,7 +534,7 @@ module OllamaChat::PersonaeManagement
532
534
  # @return [self, nil] returns self on success, or nil if the operation was
533
535
  # cancelled during persona selection or name entry.
534
536
  def duplicate_persona
535
- persona = choose_persona or return
537
+ persona = choose_persona(prompt: 'Which persona shall serve as the blueprint? ') or return
536
538
  pathname = persona_name_to_pathname(persona)
537
539
  new_persona_name = determine_valid_new_name_for_persona('to ducplicate as') or return
538
540
  new_pathname = persona_name_to_pathname(new_persona_name)
@@ -584,7 +586,7 @@ module OllamaChat::PersonaeManagement
584
586
  # @return [self, nil] returns self if the export was successful, or nil if
585
587
  # the process was cancelled during persona selection or filename entry.
586
588
  def export_persona
587
- persona = choose_persona or return
589
+ persona = choose_persona(prompt: 'Which persona are you taking with you? ') or return
588
590
  pathname = persona_name_to_pathname(persona)
589
591
  content = pathname.read
590
592
  STDOUT.puts kramdown_ansi_parse(
@@ -14,8 +14,6 @@ module OllamaChat::PromptHandling
14
14
  models::Prompt.where(context: 'system_prompt', name: name.to_s).first
15
15
  end
16
16
 
17
- private
18
-
19
17
  # Retrieves a specific prompt by name from the 'prompt' context.
20
18
  #
21
19
  # @param name [String, Symbol] the name of the prompt to retrieve
@@ -25,6 +23,8 @@ module OllamaChat::PromptHandling
25
23
  models::Prompt.where(context: 'prompt', name: name.to_s).first
26
24
  end
27
25
 
26
+ private
27
+
28
28
  # Iterates over all prompts in the 'prompt' context.
29
29
  #
30
30
  # @yield [prompt] yields each prompt model instance
@@ -26,13 +26,14 @@ module OllamaChat::PromptManagement
26
26
  #
27
27
  # @param default [Boolean, nil] filter for default prompts (true: only
28
28
  # defaults, false: only non-defaults)
29
+ # @param prompt [String] the prompt message to display when asking for input
29
30
  #
30
31
  # @return [OllamaChat::Database::Models::Prompt, nil] the selected prompt
31
32
  # model, or nil if the user chooses '[EXIT]' or cancels the selection.
32
- def choose_prompt(default: nil)
33
+ def choose_prompt(default: nil, prompt: 'Select a prompt template: ')
33
34
  prompts = all_prompts(default: default)
34
35
  prompts.unshift('[EXIT]')
35
- case chosen = choose_entry(prompts)
36
+ case chosen = choose_entry(prompts, prompt:)
36
37
  when '[EXIT]', nil
37
38
  STDOUT.puts "Exiting chooser."
38
39
  return
@@ -45,7 +46,7 @@ module OllamaChat::PromptManagement
45
46
  #
46
47
  # @return [self, nil] the current context on success, or nil if cancelled
47
48
  def info_prompt
48
- if prompt = choose_prompt
49
+ if prompt = choose_prompt(prompt: 'Which blueprint would you like to inspect? ')
49
50
  use_pager do |output|
50
51
  output.puts kramdown_ansi_parse(<<~EOT)
51
52
  # Prompt #{prompt.name}
@@ -98,7 +99,7 @@ module OllamaChat::PromptManagement
98
99
  # Interactively selects an existing non-default prompt and deletes it after
99
100
  # confirmation.
100
101
  def choose_and_delete_prompt
101
- prompt = choose_prompt(default: false) or return
102
+ prompt = choose_prompt(default: false, prompt: 'Which template has outlived its usefulness? ') or return
102
103
  STDOUT.puts kramdown_ansi_parse(
103
104
  prompt.to_s + "\n---"
104
105
  )
@@ -114,7 +115,7 @@ module OllamaChat::PromptManagement
114
115
  #
115
116
  # @return [self, nil] the current context on success, or nil if cancelled
116
117
  def choose_and_edit_prompt
117
- prompt = choose_prompt or return
118
+ prompt = choose_prompt(prompt: 'Which spell needs some fine-tuning? ') or return
118
119
  prompt.metadata['content'] = edit_text(prompt.metadata['content'].to_s)
119
120
  prompt.save
120
121
  self
@@ -132,7 +133,7 @@ module OllamaChat::PromptManagement
132
133
  # @return [self, nil] the current context on success, or nil if the user
133
134
  # cancelled the operation or no prompt was selected.
134
135
  def duplicate_prompt
135
- prompt = choose_prompt or return
136
+ prompt = choose_prompt(prompt: 'Which prompt shall be the basis for a new one? ') or return
136
137
  STDOUT.puts kramdown_ansi_parse(
137
138
  prompt.to_s + "\n---"
138
139
  )
@@ -203,7 +204,7 @@ module OllamaChat::PromptManagement
203
204
  # @return [self, nil] returns self if the export was successful, or nil if
204
205
  # the process was cancelled during prompt selection or filename entry.
205
206
  def export_prompt
206
- prompt = choose_prompt or return
207
+ prompt = choose_prompt(prompt: 'Which template are you exporting to disk? ') or return
207
208
  STDOUT.puts kramdown_ansi_parse(
208
209
  prompt.to_s + "\n---"
209
210
  )
@@ -213,6 +214,47 @@ module OllamaChat::PromptManagement
213
214
  self
214
215
  end
215
216
 
217
+ # Aggregates the current conversation history into a single string for
218
+ # context-aware generation.
219
+ #
220
+ # Each message is formatted as "Sender Name: Message Content",
221
+ # skipping messages that contain no content.
222
+ #
223
+ # @return [String] The flattened conversation history.
224
+ def prepare_conversation_history
225
+ messages.each_message.inject('') do |result, message|
226
+ message_content = message.content.full? or next result
227
+ sender_name = sender_name_displayed(message, template: false)
228
+ result << "%s: %s" % [ sender_name, message_content ]
229
+ end
230
+ end
231
+
232
+ # Interactively generates follow-up prompt suggestions based on the current
233
+ # session.
234
+ #
235
+ # This method prompts the user to select a suggestion strategy (e.g., coding
236
+ # or roleplaying), constructs a prompt containing the conversation history,
237
+ # and requests a generation from the AI model. The resulting suggestions
238
+ # are then opened in the editor for final refinement before being returned.
239
+ #
240
+ # @return [String, nil] The refined suggestion text, or nil if the process
241
+ # was cancelled.
242
+ def suggest_prompts
243
+ # Let the user pick a prompt template (e.g., suggest_coding, suggest_roleplaying)
244
+ instruction = choose_prompt(prompt: 'Which suggestion strategy shall we employ? ') or return
245
+
246
+ # Build the context by gathering all current conversation messages
247
+ history = prepare_conversation_history
248
+ full_prompt = "Conversation History:\n#{history}\n\n#{instruction}"
249
+
250
+ # Execute a silent generation call (doesn't add to history)
251
+ response = generate(prompt: full_prompt)
252
+ suggestions = response.response
253
+
254
+ # Pass the AI's suggestions through the editor for final refinement
255
+ edit_text(suggestions)
256
+ end
257
+
216
258
  # Lists all prompt templates in the database, indicating which are defaults
217
259
  # and showing a truncated preview of their content.
218
260
  #
@@ -235,7 +277,7 @@ module OllamaChat::PromptManagement
235
277
  # Resets a prompt's content to the default value defined in the configuration.
236
278
  #
237
279
  # @param name [String, Symbol] the name of the prompt to reset
238
- # @return [Boolean] true if the prompt was reset, false if no default was found
280
+ # @return [Boolean, nil] true if the prompt was reset, false if no default was found
239
281
  def reset_prompt_to_default(name)
240
282
  if content = config.prompts[name.to_s]
241
283
  store_prompt(name, content)
@@ -42,7 +42,10 @@ module OllamaChat::RAGHandling
42
42
  choose_with_state do
43
43
  loop do
44
44
  tags = @documents.tags.to_a.unshift('[ALL]').unshift('[EXIT]')
45
- tag = choose_entry(tags, prompt: 'Clear? %s')
45
+ tag = choose_entry(
46
+ tags,
47
+ prompt: 'What obsolete records are to be excised from the annals? '
48
+ )
46
49
  case tag
47
50
  when nil, '[EXIT]'
48
51
  STDOUT.puts "Exiting chooser."
@@ -80,7 +83,10 @@ module OllamaChat::RAGHandling
80
83
  collections = [ current_collection ] + @documents.collections.to_a
81
84
  collections = collections.filter_map(&:to_s).uniq.sort
82
85
  collections.unshift('[EXIT]').unshift('[NEW]')
83
- collection = choose_entry(collections) || current_collection
86
+ collection = choose_entry(
87
+ collections,
88
+ prompt: 'Which archive of knowledge shall we delve into?'
89
+ ) || current_collection
84
90
  case collection&.to_s
85
91
  when '[NEW]'
86
92
  @documents.collection = ask?(
@@ -506,7 +506,7 @@ module OllamaChat::SessionManagement
506
506
  else
507
507
  offer_new_session and sessions.unshift(SearchUI::Wrapper.new('[new]', display: '[NEW]'))
508
508
  sessions = sessions.unshift(SearchUI::Wrapper.new('[exit]', display: '[EXIT]'))
509
- value = choose_entry(sessions)&.value
509
+ value = choose_entry(sessions, prompt: 'Select a chat session: ')&.value
510
510
  if value == '[new]'
511
511
  return new_session
512
512
  end
@@ -90,7 +90,11 @@ module OllamaChat::StateSelectors
90
90
  SearchUI::Wrapper.new(state, display:)
91
91
  end
92
92
 
93
- case chosen = choose_entry(states)
93
+ chosen = choose_entry(
94
+ states,
95
+ prompt: 'Which operational paradigm should be engaged?'
96
+ )
97
+ case chosen
94
98
  when '[EXIT]', nil
95
99
  STDOUT.puts "Exiting chooser."
96
100
  when
@@ -85,7 +85,10 @@ module OllamaChat::SystemPromptManagement
85
85
  def change_system_prompt(default)
86
86
  prompts = all_system_prompts
87
87
  prompts.unshift('[MODEL DEFAULT]').unshift('[EXIT]')
88
- chosen = choose_entry(prompts)
88
+ chosen = choose_entry(
89
+ prompts,
90
+ prompt: 'Which governing law shall we enact?'
91
+ )
89
92
  system_prompt_name =
90
93
  case chosen
91
94
  when '[EXIT]'
@@ -105,11 +108,14 @@ module OllamaChat::SystemPromptManagement
105
108
 
106
109
  # Presents an interactive menu to select a stored system prompt.
107
110
  #
111
+ # @param prompt [String] the prompt message to display when asking for input
112
+ # (default: 'Select a system prompt: ')
113
+ #
108
114
  # @return [Object, nil] the selected system prompt object, or nil if cancelled
109
- def choose_system_prompt
115
+ def choose_system_prompt(prompt: 'Select a system prompt: ')
110
116
  prompts = all_system_prompts
111
117
  prompts.unshift('[EXIT]')
112
- case chosen = choose_entry(prompts)
118
+ case chosen = choose_entry(prompts, prompt:)
113
119
  when '[EXIT]', nil
114
120
  STDOUT.puts "Exiting chooser."
115
121
  return
@@ -122,7 +128,7 @@ module OllamaChat::SystemPromptManagement
122
128
  #
123
129
  # @return [self, nil] the current context on success, or nil if cancelled
124
130
  def info_system_prompt
125
- if system_prompt = choose_system_prompt
131
+ if system_prompt = choose_system_prompt(prompt: 'Which system law would you like to review? ')
126
132
  use_pager do |output|
127
133
  output.puts kramdown_ansi_parse(<<~EOT)
128
134
  # System Prompt #{system_prompt.name}
@@ -163,7 +169,9 @@ module OllamaChat::SystemPromptManagement
163
169
  #
164
170
  # @return [self, nil] the current context on success, or nil if cancelled
165
171
  def choose_and_edit_system_prompt
166
- system_prompt = choose_system_prompt or return
172
+ system_prompt = choose_system_prompt(
173
+ prompt: 'Which system directive needs rewriting? '
174
+ ) or return
167
175
  system_prompt.metadata['content'] = edit_text(system_prompt.metadata['content'].to_s)
168
176
  system_prompt.save
169
177
  ask_to_set_current_system_prompt(system_prompt.name)
@@ -175,7 +183,7 @@ module OllamaChat::SystemPromptManagement
175
183
  #
176
184
  # @return [self, nil] the current context on success, or nil if cancelled
177
185
  def choose_and_delete_system_prompt
178
- system_prompt = choose_system_prompt or return
186
+ system_prompt = choose_system_prompt(prompt: 'Which old rule is now obsolete? ') or return
179
187
  STDOUT.puts kramdown_ansi_parse(
180
188
  system_prompt.to_s + "\n---"
181
189
  )
@@ -218,7 +226,7 @@ module OllamaChat::SystemPromptManagement
218
226
  # @return [self, nil] the current context on success, or nil if the user
219
227
  # cancelled the operation or no system prompt was selected.
220
228
  def duplicate_system_prompt
221
- system_prompt = choose_system_prompt or return
229
+ system_prompt = choose_system_prompt(prompt: 'Which core logic shall be cloned? ') or return
222
230
  STDOUT.puts kramdown_ansi_parse(
223
231
  system_prompt.to_s + "\n---"
224
232
  )
@@ -273,7 +281,7 @@ module OllamaChat::SystemPromptManagement
273
281
  # @return [self, nil] returns self if the export was successful, or nil if
274
282
  # the process was cancelled during system prompt selection or filename entry.
275
283
  def export_system_prompt
276
- prompt = choose_system_prompt or return
284
+ prompt = choose_system_prompt(prompt: 'Which system prompt are you archiving to disk? ') or return
277
285
  STDOUT.puts kramdown_ansi_parse(
278
286
  prompt.to_s + "\n---"
279
287
  )
@@ -286,7 +294,8 @@ module OllamaChat::SystemPromptManagement
286
294
  # Resets a system prompt's content to the default value defined in the configuration.
287
295
  #
288
296
  # @param name [String, Symbol] the name of the system prompt to reset
289
- # @return [Boolean] true if the system prompt was reset, false if no default was found
297
+ # @return [Boolean, nil] true if the system prompt was reset, false if no
298
+ # default was found
290
299
  def reset_system_prompt_to_default(name)
291
300
  if content = config.system_prompts[name.to_s]
292
301
  store_system_prompt(name, content)
@@ -128,7 +128,11 @@ module OllamaChat::ToolCalling
128
128
  loop do
129
129
  select_tools = configured_tools - enabled_tools
130
130
  select_tools = [ '[EXIT]' ] + select_tools
131
- case chosen = choose_entry(select_tools)
131
+ chosen = choose_entry(
132
+ select_tools,
133
+ prompt: 'Which capabilities should be granted to the model?'
134
+ )
135
+ case chosen
132
136
  when '[EXIT]', nil
133
137
  STDOUT.puts "Exiting chooser."
134
138
  return
@@ -155,7 +159,11 @@ module OllamaChat::ToolCalling
155
159
  loop do
156
160
  select_tools = enabled_tools
157
161
  select_tools = [ '[EXIT]' ] + select_tools
158
- case chosen = choose_entry(select_tools)
162
+ chosen = choose_entry(
163
+ select_tools,
164
+ prompt: 'Which capabilities should be granted to the model?'
165
+ )
166
+ case chosen
159
167
  when '[EXIT]', nil
160
168
  STDOUT.puts "Exiting chooser."
161
169
  return
@@ -18,6 +18,7 @@
18
18
  # the underlying document store.
19
19
  class OllamaChat::Tools::RetrieveDocumentSnippets
20
20
  include OllamaChat::Tools::Concern
21
+ include Kramdown::ANSI::Width
21
22
 
22
23
  # @return [String] the registered name for this tool
23
24
  def self.register_name = 'retrieve_document_snippets'
@@ -74,6 +75,10 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
74
75
  text_count: Tool::Function::Parameters::Property.new(
75
76
  type: 'integer',
76
77
  description: 'The maximum number of snippets to return.'
78
+ ),
79
+ rerank: Tool::Function::Parameters::Property.new(
80
+ type: 'boolean',
81
+ description: 'Rerank the returned records if true, (default: true)'
77
82
  )
78
83
  },
79
84
  required: ['query']
@@ -102,6 +107,8 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
102
107
  text_size = args.text_size.full? || chat.config.embedding.found_texts_size?
103
108
  text_count = args.text_count.full? || chat.config.embedding.found_texts_count?
104
109
  min_similarity = args.min_similarity.full?
110
+ rerank = args.rerank
111
+ rerank = true if rerank.nil?
105
112
 
106
113
  old_collection = nil
107
114
 
@@ -112,14 +119,19 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
112
119
 
113
120
  records = find_document_records(chat, query, tags, text_size, text_count, min_similarity)
114
121
 
122
+ if rerank && records.any?
123
+ records = rerank_records(chat, query, records)
124
+ end
125
+
115
126
  message = records.map { |record|
116
127
  link = if record.source =~ %r(\Ahttps?://)
117
128
  record.source
118
129
  else
119
130
  'file://%s' % File.expand_path(record.source)
120
131
  end
132
+ link && record.tags.any? or next
121
133
  [ link, ?# + record.tags.first ]
122
- }.uniq.map { |l, t| chat.hyperlink(l, t) }.join(' ')
134
+ }.flat_map { |l, t| chat.hyperlink(l, t) }.join(' ')
123
135
 
124
136
  {
125
137
  prompt: 'Consider these snippets generated from retrieval when formulating your response!',
@@ -131,6 +143,12 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
131
143
  }
132
144
  end,
133
145
  message:,
146
+ query:,
147
+ tags:,
148
+ min_similarity:,
149
+ text_size:,
150
+ text_count:,
151
+ rerank:,
134
152
  }.to_json
135
153
  rescue => e
136
154
  { error: e.class.name, message: e.message }.to_json
@@ -140,6 +158,39 @@ class OllamaChat::Tools::RetrieveDocumentSnippets
140
158
 
141
159
  private
142
160
 
161
+ # Uses the active chat model to filter records based on the query.
162
+ #
163
+ # @param chat [OllamaChat::Chat] the active chat instance
164
+ # @param query [String] the search query string
165
+ # @param records [Array<Documentrix::Utils::TagResult>] the initial set of
166
+ # found records
167
+ #
168
+ # @return [Array<Documentrix::Utils::TagResult>] the filtered array of
169
+ # records
170
+ #
171
+ # @raise [RuntimeError] if the 'rerank' prompt is missing from the
172
+ # configuration
173
+ def rerank_records(chat, query, records)
174
+ candidates = records.each_with_index.map { |r, i|
175
+ "[#{i}] #{truncate(r.text.strip, length: 300)}"
176
+ }.join("\n")
177
+
178
+ prompt = chat.prompt('rerank') or raise "missing prompt 'rerank'"
179
+ prompt = prompt.to_s % { query:, candidates: }
180
+
181
+ begin
182
+ # We use the active chat model to perform the surgical precision
183
+ # filtering
184
+ if response = chat.generate(prompt:)&.response.full?
185
+ indices = response.scan(/\d+/).map(&:to_i).select { |i| (0...records.size).include?(i) }
186
+ records = records.values_at(*indices) if indices.any?
187
+ end
188
+ rescue => e
189
+ chat.log(:error, "Attempted reranking, caught #{e.class} #{e}")
190
+ end
191
+ records
192
+ end
193
+
143
194
  # The find_document_records method searches for document records matching the
144
195
  # given query string.
145
196
  #
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.91'
3
+ VERSION = '0.0.92'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/ollama_chat.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: ollama_chat 0.0.91 ruby lib
2
+ # stub: ollama_chat 0.0.92 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.91".freeze
6
+ s.version = "0.0.92".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -49,7 +49,10 @@ describe OllamaChat::InputContent do
49
49
 
50
50
  # Mock the selection process
51
51
  expect(chat).to receive(:choose_entry).
52
- with(files.unshift('[EXIT]')).and_return(files[1])
52
+ with(
53
+ files.unshift('[EXIT]'),
54
+ prompt: 'Select a file to import: ',
55
+ ).and_return(files[1])
53
56
 
54
57
  result = chat.choose_filename('spec/assets/**/*.txt')
55
58
  expect(result).to eq Pathname.new(files[1])
@@ -123,7 +123,8 @@ describe OllamaChat::StateSelectors::StateSelector do
123
123
  it 'allows user to select a state from available options' do
124
124
  # Mock the chooser to return a specific choice
125
125
  expect(selector).to receive(:choose_entry).with(
126
- %w[ [EXIT] enabled disabled low high ]
126
+ %w[ [EXIT] enabled disabled low high ],
127
+ prompt: 'Which operational paradigm should be engaged?'
127
128
  ).and_return(double(value: 'low'))
128
129
 
129
130
  selector.choose
@@ -25,6 +25,7 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
25
25
  min_similarity: nil,
26
26
  text_size: nil,
27
27
  text_count: nil,
28
+ rerank: false,
28
29
  )
29
30
  )
30
31
  )
@@ -68,6 +69,7 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
68
69
  min_similarity: nil,
69
70
  text_size: nil,
70
71
  text_count: nil,
72
+ rerank: false,
71
73
  )
72
74
  )
73
75
  )
@@ -79,7 +81,7 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
79
81
  [
80
82
  double(
81
83
  'Record',
82
- text: 'quux',
84
+ text: 'quintessential ruby',
83
85
  source: 'foo',
84
86
  tags: %w[ ruby expert ],
85
87
  tags_set: [],
@@ -107,6 +109,7 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
107
109
  min_similarity: nil,
108
110
  text_size: nil,
109
111
  text_count: nil,
112
+ rerank: false,
110
113
  )
111
114
  )
112
115
  )
@@ -141,6 +144,78 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
141
144
  expect(json.message).to eq('Empty query')
142
145
  end
143
146
 
147
+ it 'performs reranking when rerank is true' do
148
+ tool_call = double(
149
+ 'ToolCall',
150
+ function: double(
151
+ name: 'retrieve_document_snippets',
152
+ arguments: double(
153
+ query: 'Ruby array',
154
+ tags: nil,
155
+ collection: nil,
156
+ min_similarity: nil,
157
+ text_size: nil,
158
+ text_count: nil,
159
+ rerank: true,
160
+ )
161
+ )
162
+ )
163
+
164
+ records = [
165
+ double('Record', text: 'first', source: 's1', tags: [], tags_set: [], similarity: 0.1),
166
+ double('Record', text: 'second', source: 's2', tags: [], tags_set: [], similarity: 0.9)
167
+ ]
168
+
169
+ tool = described_class.new
170
+ expect(tool).to receive(:find_document_records).and_return(records)
171
+
172
+ allow(chat).to receive(:prompt).with('rerank').and_return("template %{query} %{candidates}")
173
+ response_val = double('Response', response: double('FullResponse', full?: '1', response: '1'))
174
+ allow(chat).to receive(:generate).with(prompt: anything).and_return(response_val)
175
+
176
+ result = tool.execute(tool_call, chat:)
177
+ json = json_object(result)
178
+
179
+ expect(json.snippets.size).to eq 1
180
+ expect(json.snippets.first.text).to eq 'second'
181
+ end
182
+
183
+ it 'performs reranking when rerank is nil (defaults to true)' do
184
+ tool_call = double(
185
+ 'ToolCall',
186
+ function: double(
187
+ name: 'retrieve_document_snippets',
188
+ arguments: double(
189
+ query: 'Ruby array',
190
+ tags: nil,
191
+ collection: nil,
192
+ min_similarity: nil,
193
+ text_size: nil,
194
+ text_count: nil,
195
+ rerank: nil,
196
+ )
197
+ )
198
+ )
199
+
200
+ records = [
201
+ double('Record', text: 'first', source: 's1', tags: [], tags_set: [], similarity: 0.1),
202
+ double('Record', text: 'second', source: 's2', tags: [], tags_set: [], similarity: 0.9)
203
+ ]
204
+
205
+ tool = described_class.new
206
+ expect(tool).to receive(:find_document_records).and_return(records)
207
+
208
+ allow(chat).to receive(:prompt).with('rerank').and_return("template %{query} %{candidates}")
209
+ response_val = double('Response', response: double('FullResponse', full?: '0', response: '0'))
210
+ allow(chat).to receive(:generate).with(prompt: anything).and_return(response_val)
211
+
212
+ result = tool.execute(tool_call, chat:)
213
+ json = json_object(result)
214
+
215
+ expect(json.snippets.size).to eq 1
216
+ expect(json.snippets.first.text).to eq 'first'
217
+ end
218
+
144
219
  it 'can be converted to hash' do
145
220
  expect(described_class.new.to_hash).to be_a(Hash)
146
221
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ollama_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.91
4
+ version: 0.0.92
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank