ollama_chat 0.0.115 → 0.0.116

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: 18297e3bbf39619c1721b30fdf4f691a2a71b1e15994c2ae28ced14eb7f76c57
4
- data.tar.gz: 1481131a053ef2d9390456c84e9d58bf0890d1ded23eafb5d15c4b1b70babb99
3
+ metadata.gz: beebfc1a29093b596fe008ba59b2f377922e2f7beeeb529ebed899fc374c6e02
4
+ data.tar.gz: e704bce9b15101283e802a6010ee08c347cf94d8fcdc2ee785da3880debde736
5
5
  SHA512:
6
- metadata.gz: 6ec5d58d329bb3b8a25ccdb0ce55cb04be29f5e8b22e2b3f7aa8ce77bfe49cf56d33c6c198c63431b4ccca9e78029887fe221be1effa5306d4aef464fe530fd9
7
- data.tar.gz: ec410eb4d78a5688cb4b67bec44a3600a8e295578bb9375142957577b92d03c5ae0ada7f0e4f7841b70ed40c9dea8e6f1acc688d76abc6ba4d156c550cbf3324
6
+ metadata.gz: 7cab4e5d47d97dd215e058ab9960ad8801252a2ab950d3b30ce1e514fad30b22e87f917e3b1764592070f385fcfec748cc3b59398cadf8e3553f41e6ec48e9bb
7
+ data.tar.gz: 0fb71bb0ab178cb1fed41ea2b257360fd19d3d180a2fc2f497d1372874d4665c3b82676ad8c056ab80a22e21e95deebb8afee8ef5f2809a07671e6cfb53ba5d7
data/CHANGES.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-03 v0.0.116
4
+
5
+ ### Added
6
+
7
+ * `session.roleplay_story` prompt in `default_config.yml` for narrative
8
+ retelling of roleplay sessions via `/conversation report`.
9
+ * `session.coding_brief` prompt in `default_config.yml`.
10
+ * `summarize` and `report` subcommands to `/conversation`.
11
+ * `summarize_conversation(sentence:, save:)` and `report_conversation(name:,
12
+ save:)` methods in `session_management.rb` to handle filename prompts,
13
+ overwrite checks, and I/O.
14
+ * `report_session(name:, &block)` method using `messages.compacted_messages` to
15
+ keep interpolated content bounded after compaction.
16
+ * `Status:` line with `✅`/`⛔` indicators to `collection_stats` in
17
+ `information.rb`.
18
+ * `✅`/`⛔` prefixes to `list_collections` in `rag_handling.rb` to replace
19
+ `[DISABLED]` text suffixes.
20
+ * `✅`/`⛔` indicators for enabled/disabled states and `❔`/`⭕` for
21
+ confirmation requirements in `list_tools` in `tool_calling.rb`.
22
+ * `ask_for_filename?(action:)` and `should_overwrite?(filename)` helpers in
23
+ `dialog.rb` with YARD documentation.
24
+ * Specs for `summarize_conversation`, `report_session`, and
25
+ `report_conversation` in `session_management_spec.rb`.
26
+ * Routing specs for `/conversation summarize` and `/conversation report` in
27
+ `commands_spec.rb`.
28
+ * YARD documentation for `repair_group_uuids`.
29
+
30
+ ### Changed
31
+
32
+ * Moved `summarize` subcommand from `/session` to `/conversation`.
33
+ * Reworked compaction prompt to a draft-rewrite model, treating previous
34
+ summaries as drafts to be rewritten rather than ambient context.
35
+ * Replaced "6-12 sentences" constraint in compaction prompt with "Be concise;
36
+ skip filler and repetition".
37
+ * Changed `assemble: |-` to `assemble: |` in compaction prompt to preserve
38
+ trailing newlines.
39
+ * Widened factuality scope in compaction prompt to include "groups or previous
40
+ summary above".
41
+ * Wrapped `list_tools` body in `use_pager`, redirecting writes through the
42
+ yielded `output` IO.
43
+ * Passed `output:` keyword argument to `tools_support.show(output:)`.
44
+ * Replaced inline `File.exist?` and `confirm?` blocks in
45
+ `conversation.rb#save_conversation`,
46
+ `message_output.rb#attempt_to_write_file`, `session_management.rb`, and
47
+ `file_editing.rb#determine_valid_output_filename` with the shared
48
+ `should_overwrite?` helper.
49
+ * Updated `chat_spec.rb` `collection_stats` regex to include the new `Status:
50
+ ✅` line.
51
+ * Updated `tool_calling_spec.rb` `list_tools` regex to use `✅`/`⛔` instead of
52
+ `✓`/`☐`.
53
+ * Updated `tool_calling_spec.rb` `#list_tools` example to stub `use_pager` with
54
+ a `StringIO` yield.
55
+ * Updated `conversation_spec.rb` expectations for `Pathname` type and
56
+ `should_overwrite?` STDERR output.
57
+ * Updated comment in `lib/ollama_chat/prompt_management.rb` to reflect new
58
+ naming convention for prompt templates (e.g., `suggest.coding`).
59
+ * Rewrapped `prompts.suggest.roleplaying` few-shot examples to stay within 80
60
+ columns in `default_config.yml`.
61
+ * Fixed stale YARD example in `session_management.rb#report_session`
62
+ (`roleplay_report` to `roleplay_story`).
63
+
64
+ ### Removed
65
+
66
+ * `pretty:` keyword argument from `summarize_session` and
67
+ `summarize_conversation`; markdown formatting is now the only path.
68
+ * `%{message_thinking}` from the `session_summarize` prompt.
69
+
3
70
  ## 2026-09-01 v0.0.115
4
71
 
5
72
  * Added `OllamaChat::Tools::ExecuteShell` in
@@ -291,15 +291,14 @@ module OllamaChat::Commands
291
291
 
292
292
  command(
293
293
  name: :session,
294
- regexp: %r(^/session(?:\s+(change|previous|list|new|duplicate|rename|summarize|delete|model options change|model options))?((?:\s+-(?:[sf]|p\s*\w+))*)(?:\s+([^-].*))?$),
295
- complete: [ 'session', %w[ change previous list new duplicate rename summarize delete model\ options\ change model\ options ] ],
294
+ regexp: %r(^/session(?:\s+(change|previous|list|new|duplicate|rename|delete|model options change|model options))?((?:\s+-(?:p\s*\w+))*)(?:\s+([^-].*))?$),
295
+ complete: [ 'session', %w[ change previous list new duplicate rename delete model\ options\ change model\ options ] ],
296
296
  optional: true,
297
- options: "[-s|-f|-p profile]\n[name]",
297
+ options: "[-p profile]\n[name]",
298
298
  help: <<~EOT
299
299
  💬 Manage sessions:
300
300
  - list/new/delete/rename/duplicate
301
301
  - change [name]/previous
302
- - summarize (-s sentence, -f save to file)
303
302
  - model options/change
304
303
  EOT
305
304
  ) do |subcommand, opts, name|
@@ -316,44 +315,6 @@ module OllamaChat::Commands
316
315
  delete_session
317
316
  when 'rename'
318
317
  rename_session
319
- when 'summarize'
320
- opts = go_command('fs', opts)
321
- if opts[?f] and
322
- filename = switch_history(:filename) {
323
- ask?(prompt: "❓ Enter filename: ")
324
- }.full? { Pathname.new(_1) }
325
- then
326
- if filename.exist? && !confirm?(
327
- prompt: "🔔 File #{filename.to_s.inspect} already exists, overwrite? (y/n) ",
328
- yes: /\Ay/i
329
- )
330
- then
331
- STDERR.puts "File not written!"
332
- next :next
333
- end
334
- summary = summarize_session(pretty: true, sentence: opts[?s]) do |content|
335
- infobar.puts kramdown_ansi_parse(content)
336
- end
337
- if summary.full?
338
- filename.write(summary)
339
- STDOUT.puts "File successfully written."
340
- next :next
341
- else
342
- STDERR.puts "Nothing to summarize!"
343
- next :next
344
- end
345
- end
346
- summary = summarize_session(pretty: true, sentence: opts[?s]) do |content|
347
- infobar.puts kramdown_ansi_parse(content) << ?\n
348
- end
349
- if summary.full?
350
- use_pager do |output|
351
- output.puts kramdown_ansi_parse(summary)
352
- end
353
- else
354
- STDERR.puts "Nothing to summarize!"
355
- next :next
356
- end
357
318
  when 'change'
358
319
  change_session(name)
359
320
  when 'model options'
@@ -598,15 +559,16 @@ module OllamaChat::Commands
598
559
 
599
560
  command(
600
561
  name: :conversation,
601
- regexp: %r(^/conversation\s+(clean|compact|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
602
- complete: [ 'conversation', %w[ save load clean compact ] ],
603
- options: '[-c] [FILENAME]',
562
+ regexp: %r(^/conversation\s+(clean|compact|save|load|summarize|report)((?:\s+-(?:[sf]|c))*)(?:\s+([^-].*\.jsonl?))?$),
563
+ complete: [ 'conversation', %w[ save load clean compact summarize report ] ],
564
+ options: '[-s|-f|-c] [FILENAME]',
604
565
  help: <<~EOT
605
- 💾 Save/Load conversation state
606
- (-c to clean before saving)
607
- FILENAME ends with .json or .jsonl
608
- Or clean inplace (removes tool content, images, and thinking)
609
- Or compact (summarize old messages, keep recent)
566
+ 💾 Manage conversation content:
567
+ - save/load: Export/import as .json or .jsonl
568
+ - clean: Remove tool content, images, thinking
569
+ - compact: Summarize old messages, keep recent
570
+ - summarize: Per-message narrative (-s sentence, -f save)
571
+ - report: Generate a session report document
610
572
  EOT
611
573
  ) do |subcommand,opts,path|
612
574
  if %w[ save load ].include?(subcommand) && path.blank?
@@ -642,6 +604,12 @@ module OllamaChat::Commands
642
604
  else
643
605
  STDOUT.puts 'Cancelled.'
644
606
  end
607
+ when 'summarize'
608
+ opts = go_command('sf', opts)
609
+ summarize_conversation(save: opts[?f], sentence: opts[?s])
610
+ when 'report'
611
+ opts = go_command('f', opts)
612
+ report_conversation(save: opts[?f])
645
613
  end
646
614
  :next
647
615
  end
@@ -26,12 +26,8 @@ module OllamaChat::Conversation
26
26
  # @example Save conversation with explicit filename
27
27
  # chat.save_conversation('conversations/2023-10-15_my_session.json')
28
28
  def save_conversation(filename, clean: false)
29
- if File.exist?(filename)
30
- confirm?(
31
- prompt: "🔔 File #{filename.to_s.inspect} already exists, overwrite? (y/n) ",
32
- yes: /\Ay/i
33
- ) or return
34
- end
29
+ filename = Pathname.new(filename)
30
+ should_overwrite?(filename) or return
35
31
  if messages.save_conversation(filename, messages: clean ? messages.clean_messages : messages.messages)
36
32
  STDOUT.puts "Saved conversation to #{filename.to_s.inspect}."
37
33
  else
@@ -149,4 +149,38 @@ module OllamaChat::Dialog
149
149
  def go_command(s, opt, defaults: {})
150
150
  Tins::GO.go(s, opt.to_s.strip.split(/\s+/), defaults:)
151
151
  end
152
+
153
+ # Prompts the user for a filename with history support.
154
+ #
155
+ # @param action [String, nil] an optional action descriptor appended to the
156
+ # prompt, e.g. "for summarization" ("❓ Enter filename for summarization…")
157
+ #
158
+ # @return [Pathname, nil] the user-supplied filename as a Pathname, or nil
159
+ # if the user cancels (C-c) or provides empty input
160
+ def ask_for_filename?(action: nil)
161
+ action and action = " #{action}"
162
+ switch_history(:filename) {
163
+ ask?(prompt: "❓ Enter filename#{action}, C-c ⇒ cancel: ")
164
+ }.full? { Pathname.new(_1) }
165
+ end
166
+
167
+ # Checks whether a file can be written, prompting for overwrite
168
+ # confirmation if the file already exists.
169
+ #
170
+ # @param filename [Pathname] the target file to check
171
+ #
172
+ # @return [Boolean] `true` if the file does not exist or the user confirms
173
+ # overwriting; `false` if the file exists and the user declines
174
+ def should_overwrite?(filename)
175
+ if filename.exist? && !confirm?(
176
+ prompt: "🔔 File #{filename.to_s.inspect} already exists, overwrite? (y/n) ",
177
+ yes: /\Ay/i
178
+ )
179
+ then
180
+ STDERR.puts "File not written!"
181
+ false
182
+ else
183
+ true
184
+ end
185
+ end
152
186
  end
@@ -113,15 +113,12 @@ module OllamaChat::FileEditing
113
113
  def determine_valid_output_filename(action)
114
114
  switch_history(:filename) do
115
115
  loop do
116
- filename_str = ask?(
117
- prompt: "❓ Enter filename #{action}, C-c ⇒ cancel: "
118
- )
119
- if filename_str.nil?
116
+ filename = ask_for_filename?(action:)
117
+ unless filename
120
118
  STDOUT.puts "Cancelled."
121
119
  return nil
122
120
  end
123
121
 
124
- filename = Pathname.new(filename_str)
125
122
  if filename.exist?
126
123
  STDERR.puts "File #{filename.to_path.inspect} already exists!"
127
124
  else
@@ -80,6 +80,7 @@ module OllamaChat::Information
80
80
  output.puts <<~EOT
81
81
  Current Collection
82
82
  Name: #{bold{collection}}
83
+ Status: #{col&.enabled ? '✅' : '⛔'}
83
84
  Patterns: #{italic{col&.patterns&.join(' ')}}
84
85
  #Embeddings: #{@documents.size}
85
86
  #Tags: #{@documents.tags.size}
@@ -89,17 +89,9 @@ module OllamaChat::MessageOutput
89
89
  # error occurred
90
90
  def attempt_to_write_file(filename, content)
91
91
  path = Pathname.new(filename.to_s).expand_path
92
- if !path.exist? ||
93
- confirm?(
94
- prompt: "🔔 File #{path.to_s.inspect} already exists, overwrite? (y/n) ",
95
- yes: /\Ay/i
96
- )
97
- then
98
- File.open(path, ?w) do |output|
99
- output.write(content)
100
- end
101
- else
102
- return
92
+ should_overwrite?(path) or return
93
+ File.open(path, ?w) do |output|
94
+ output.write(content)
103
95
  end
104
96
  true
105
97
  end
@@ -151,21 +151,22 @@ prompts:
151
151
  - **Format**: [Markdown, plain text, code blocks, etc.]
152
152
  - **Vocabulary**: [Technical terms, slang, formal language]
153
153
  - **Structure**: [How you organize your responses]
154
+ context_template_suggest: |
155
+ Conversation History:
156
+ %{history}
157
+
158
+ Create and output text following this instruction:
159
+ %{instruction}
154
160
  session_summarize: |
155
- Context is
161
+ Narrative so far:
156
162
 
157
163
  %{context}
158
164
 
159
- Summarize the %{sender_name} message below in one %{unit}.
160
-
161
- NEVER add any information to this message summary that already was included
162
- in the context above.
163
-
164
- Message
165
-
166
- Thinking content was
167
-
168
- %{message_thinking}
165
+ Extend the narrative above with the key contribution of the
166
+ %{sender_name} message below in one %{unit}. Skip tool-call
167
+ plumbing; focus on decisions, findings, and state changes. Do
168
+ not repeat information already present in the narrative.
169
+ Output only the summary text, without any sender name or prefix.
169
170
 
170
171
  Response content was
171
172
 
@@ -175,12 +176,26 @@ prompts:
175
176
 
176
177
  Create a title with a length of **less than %{length}** characters for
177
178
  the conversation above. Output only the title and nothing else.
178
- context_template_suggest: |
179
- Conversation History:
180
- %{history}
179
+ session:
180
+ coding_brief: |
181
+ %{content}
181
182
 
182
- Create and output text following this instruction:
183
- %{instruction}
183
+ Write a brief of the session above. Your brief MUST:
184
+ - Describe the goal and how it evolved.
185
+ - Name key decisions, files, and architectural choices.
186
+ - State the current state: what works, what is pending.
187
+ - Be factual. Be concise.
188
+ roleplay_story: |
189
+ %{content}
190
+
191
+ Write a narrative retelling of the roleplay session
192
+ above. Your story MUST:
193
+ - Capture the emotional arc and key plot beats.
194
+ - Stay in the established setting and character voices.
195
+ - Use vivid, evocative prose; show, don't tell.
196
+ - Highlight turning points, revelations, and character
197
+ growth.
198
+ - Read it like a short story chapter, not a log.
184
199
  suggest:
185
200
  coding: |
186
201
  ## Context
@@ -318,25 +333,33 @@ prompts:
318
333
 
319
334
  ## Few-Shot Examples
320
335
 
321
- Input: [Scene where a party is arguing in a rainy forest while being hunted]
322
- Output:
323
- Demand that everyone stop shouting and listen for the sound of footsteps in the brush.
336
+ Input: [Scene where a party is arguing in a rainy forest while being
337
+ hunted] Output: Demand that everyone stop shouting and listen for the
338
+ sound of footsteps in the brush.
324
339
 
325
- Quietly admit to the party leader that you are terrified, leaning into their shoulder for support.
340
+ Quietly admit to the party leader that you are terrified, leaning into
341
+ their shoulder for support.
326
342
 
327
- Examine the muddy tracks near the campfire to see if the hunters are human or something else. Roll 1d20.
343
+ Examine the muddy tracks near the campfire to see if the hunters are
344
+ human or something else. Roll 1d20.
328
345
 
329
- Secretly slip the map out of the leader's pocket while they are distracted by the argument. Roll 1d20.
346
+ Secretly slip the map out of the leader's pocket while they are
347
+ distracted by the argument. Roll 1d20.
330
348
 
331
- Raise your voice and challenge every single person present to state their true loyalty right now.
349
+ Raise your voice and challenge every single person present to state their
350
+ true loyalty right now.
332
351
 
333
- Attempt to leap across the ravine to flank the hunters, risking a fall into the rapids. Roll 1d20.
352
+ Attempt to leap across the ravine to flank the hunters, risking a fall
353
+ into the rapids. Roll 1d20.
334
354
 
335
- Obsessively polish your sword with a rag, ignoring the argument entirely until it is perfectly mirrored.
355
+ Obsessively polish your sword with a rag, ignoring the argument entirely
356
+ until it is perfectly mirrored.
336
357
 
337
- Suddenly reveal that you were actually sent here by the enemy as a double agent.
358
+ Suddenly reveal that you were actually sent here by the enemy as a double
359
+ agent.
338
360
 
339
- A sudden lightning strike hits a nearby tree, knocking everyone off balance and creating a deafening blast.
361
+ A sudden lightning strike hits a nearby tree, knocking everyone off
362
+ balance and creating a deafening blast.
340
363
 
341
364
  ## Output Format
342
365
 
@@ -356,7 +379,10 @@ prompts:
356
379
  conversation:
357
380
  %{groups}
358
381
 
359
- Summarize the conversation above. Your narrative MUST:
382
+ The previous summary (if present) is a draft. Rewrite it
383
+ to incorporate the new conversation above. The result must
384
+ read as one continuous account of the entire session, not a
385
+ concatenation of two separate summaries. Your narrative MUST:
360
386
  - Name every file created or significantly modified (with
361
387
  path), and the key methods or classes they define.
362
388
  - State the architectural or design decisions made (e.g.
@@ -366,8 +392,8 @@ prompts:
366
392
  - Do NOT merely report test counts or coverage percentages;
367
393
  those are outcomes, not accomplishments.
368
394
  - Be factual; do not invent details not present in the groups
369
- above. Keep it to 6–12 sentences.
370
- assemble: |-
395
+ or previous summary above. Be concise; skip filler and repetition.
396
+ assemble: |
371
397
  %{narrative}
372
398
 
373
399
  tool_calls:
@@ -333,7 +333,7 @@ module OllamaChat::PromptManagement
333
333
  # Let the user write a suggestion instruction on the fly
334
334
  instruction = edit_text('').full? or return
335
335
  else
336
- # Let the user pick a prompt template (e.g., suggest_coding, suggest_roleplaying)
336
+ # Let the user pick a prompt template (e.g., suggest.coding, suggest.roleplaying)
337
337
  instruction = choose_prompt(
338
338
  prompt: 'Which suggestion strategy shall we employ? %s',
339
339
  context: 'suggest'
@@ -163,10 +163,11 @@ module OllamaChat::RAGHandling
163
163
  collections = all_collections.select(:name, :description, :enabled)
164
164
  use_pager do |output|
165
165
  collections.each { |c|
166
- collection_name = current_collection == c.name ? bold { c.name } : c.name
166
+ enabled = c.enabled ? '✅' : '⛔'
167
+ collection_name = current_collection == c.name ? bold { c.name } : c.name
167
168
  collection_description = c.description
168
- suffix = c.enabled ? '' : ' [DISABLED]'
169
- output.puts '%s: %s%s' % [ collection_name, collection_description, suffix ]
169
+
170
+ output.puts '%s %s: %s' % [ enabled, collection_name, collection_description ]
170
171
  }
171
172
  end
172
173
  end
@@ -348,11 +348,10 @@ module OllamaChat::SessionManagement
348
348
 
349
349
  # Generates a summary of the current session's conversation.
350
350
  #
351
- # @param pretty [Boolean] whether to format the summary in markdown (default: false)
352
351
  # @param sentence [Boolean] whether to summarize each message in one sentence (default: false)
353
352
  # @param block [Proc] a block to handle each summary fragment
354
353
  # @return [String, nil] the session summary or nil if empty
355
- def summarize_session(pretty: false, sentence: false, &block)
354
+ def summarize_session(sentence: false, &block)
356
355
  unit = sentence ? 'sentence' : 'paragraph'
357
356
  contents = []
358
357
  messages_to_summarize = messages.each_message
@@ -362,9 +361,8 @@ module OllamaChat::SessionManagement
362
361
  message: infobar_message,
363
362
  )
364
363
  messages_to_summarize.each do |message|
365
- message_content = message.content.full?
366
- message_thinking = message.thinking.full?
367
- unless message_content || message_thinking
364
+ message_content = message.content.full?
365
+ unless message_content
368
366
  -infobar
369
367
  next
370
368
  end
@@ -373,25 +371,113 @@ module OllamaChat::SessionManagement
373
371
  context = contents * "\n\n"
374
372
  summary = generate(
375
373
  prompt: prompt(:session_summarize).to_s % {
376
- sender_name:, unit:, message_content:, message_thinking:, context:
374
+ sender_name:, unit:, message_content:, context:
377
375
  }
378
376
  )
379
- content = if pretty
380
- '**%s**: %s' % [ sender_name_output, summary ]
381
- else
382
- '%s: %s' % [ sender_name_output, summary ]
383
- end
377
+ content = '**%s**: %s' % [ sender_name_output, summary ]
384
378
  block&.(content)
385
379
  contents << content
386
380
  +infobar
387
381
  end
388
382
  contents.empty? and return
389
383
 
390
- if pretty
391
- contents.unshift(%{# Summary of session "#{session.name}"})
392
- contents * "\n\n"
384
+ contents.unshift(%{# Summary of session "#{session.name}"})
385
+ contents * "\n\n"
386
+ end
387
+
388
+ # Summarizes the conversation and displays or saves the result.
389
+ #
390
+ # @param sentence [Boolean] summarize each message in one sentence (default: false)
391
+ # @param save [Boolean] save to file instead of pager (default: false)
392
+ def summarize_conversation(sentence: false, save: false)
393
+ if save
394
+ filename = ask_for_filename?(action: 'for summarization') or return
395
+ should_overwrite?(filename) or return
396
+ summary = summarize_session(sentence:) do |content|
397
+ infobar.puts kramdown_ansi_parse(content)
398
+ end
399
+ if summary.full?
400
+ filename.write(summary)
401
+ STDOUT.puts "File successfully written."
402
+ else
403
+ STDERR.puts "Nothing to summarize!"
404
+ end
405
+ else
406
+ summary = summarize_session(sentence:) do |content|
407
+ infobar.puts kramdown_ansi_parse(content) << ?\n
408
+ end
409
+ if summary.full?
410
+ use_pager do |output|
411
+ output.puts kramdown_ansi_parse(summary)
412
+ end
413
+ else
414
+ STDERR.puts "Nothing to summarize!"
415
+ end
416
+ end
417
+ end
418
+
419
+ # Generates a report document for the current session's conversation.
420
+ #
421
+ # Uses a user-selected prompt template from the 'session' context
422
+ # (e.g. coding_brief, roleplay_story) to produce a single cohesive
423
+ # document describing the session. Uses `compacted_messages` (summary +
424
+ # recent tail) rather than `each_message` so the interpolated content
425
+ # stays bounded even after multiple compaction rounds.
426
+ #
427
+ # @param name [String, nil] specific template name (skips chooser)
428
+ # @param block [Proc] a block to handle the report content
429
+ # @return [String, nil] the report or nil if empty
430
+ def report_session(name: nil, &block)
431
+ content = messages.compacted_messages.inject('') do |c, message|
432
+ message.content.present? or next c
433
+ sender = sender_name_displayed(message)
434
+ c << "%s: %s\n\n" % [ sender, message.content ]
435
+ end
436
+ content.empty? and return
437
+
438
+ template = if name
439
+ prompt(name, context: 'session')
440
+ else
441
+ choose_prompt(
442
+ context: 'session',
443
+ prompt: 'Which report template? %s'
444
+ )
445
+ end
446
+ template or return
447
+
448
+ result = generate(prompt: template.to_s % { content: })
449
+ block&.(result)
450
+ result
451
+ end
452
+
453
+ # Generates a report and displays or saves the result.
454
+ #
455
+ # @param name [String, nil] specific report template name (skips chooser)
456
+ # @param save [Boolean] save to file instead of pager (default: false)
457
+ def report_conversation(name: nil, save: false)
458
+ if save
459
+ filename = ask_for_filename?(action: 'for report') or return
460
+ should_overwrite?(filename) or return
461
+ result = report_session(name:) do |content|
462
+ infobar.puts kramdown_ansi_parse(content)
463
+ end
464
+ if result.full?
465
+ filename.write(result)
466
+ STDOUT.puts "File successfully written."
467
+ else
468
+ STDERR.puts "Nothing to report!"
469
+ end
393
470
  else
394
- contents * ?\n
471
+ result = report_session(name:) do |content|
472
+ infobar.puts kramdown_ansi_parse(content) << ?\n
473
+ end
474
+ if result.full?
475
+ use_pager do |output|
476
+ output.puts kramdown_ansi_parse(result)
477
+ end
478
+ else
479
+ STDERR.puts "Nothing to report!"
480
+ end
395
481
  end
396
482
  end
397
483
 
@@ -575,6 +661,21 @@ module OllamaChat::SessionManagement
575
661
  end
576
662
  end
577
663
 
664
+ # Repairs `group_uuid` assignments in the current message list.
665
+ #
666
+ # Walks the message array backwards looking for user messages (that are not
667
+ # tool calls) lacking a `group_uuid`. For each such anchor it:
668
+ #
669
+ # 1. Swaps a preceding `runtime_information` tool message so the user
670
+ # message comes first (structural invariant).
671
+ # 2. Propagates the anchor's UUID forward to all subsequent messages until
672
+ # the next user message or the end of the list.
673
+ #
674
+ # After the backward pass, any remaining orphaned messages (e.g. system
675
+ # messages not part of a user-led exchange) are assigned fresh UUIDs via
676
+ # `initialize_group_uuid`.
677
+ #
678
+ # Persists the repaired message list via `store_messages_in_session`.
578
679
  def repair_group_uuids
579
680
  msgs = messages.messages # Direct reference to the internal array
580
681
  msgs.empty? and return
@@ -103,17 +103,20 @@ module OllamaChat::ToolCalling
103
103
  # This method outputs to standard output the alphabetically sorted list of
104
104
  # tool names that are currently enabled in the chat session.
105
105
  def list_tools
106
- STDOUT.puts "Registered tools:"
107
- configured_tools.each do |tool|
108
- enabled = tool_enabled?(tool) ? ?✓ : ?☐
109
- require_confirmation = tool_function(tool).require_confirmation? ? ?? : ?☐
110
- printf(
111
- "%s %s %s\n",
112
- enabled, require_confirmation, (enabled ? bold { tool } : tool)
113
- )
106
+ use_pager do |output|
107
+ output.puts "Registered tools:"
108
+ configured_tools.each do |tool|
109
+ enabled = tool_enabled?(tool) ? '✅' : '⛔'
110
+ require_confirmation = tool_function(tool).require_confirmation? ? '❔' : '⭕'
111
+
112
+ output.printf(
113
+ "%s %s %s\n",
114
+ enabled, require_confirmation, (enabled ? bold { tool } : tool)
115
+ )
116
+ end
117
+ output.puts ?┉ * Tins::Terminal.columns
118
+ tools_support.show(output:)
114
119
  end
115
- STDOUT.puts ?┉ * Tins::Terminal.columns
116
- tools_support.show
117
120
  end
118
121
 
119
122
  # The enable_tool method allows the user to select and enable a tool from a
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.115'
3
+ VERSION = '0.0.116'
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.115 ruby lib
2
+ # stub: ollama_chat 0.0.116 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.115".freeze
6
+ s.version = "0.0.116".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]
@@ -52,7 +52,7 @@ describe OllamaChat::Chat, protect_env: true do
52
52
  it 'can display collection_stats' do
53
53
  chat
54
54
  expect(STDOUT).to receive(:puts).with(
55
- /Current Collection\n Name: \e\[1mdefault\e\[0m\n Patterns: \e\[3m\e\[0m\n #Embeddings: 0\n #Tags: 0\n Tags:/
55
+ /Current Collection\n Name: \e\[1mdefault\e\[0m\n Status: ✅\n Patterns: \e\[3m\e\[0m\n #Embeddings: 0\n #Tags: 0\n Tags:/
56
56
  )
57
57
  expect(chat.collection_stats).to be_nil
58
58
  end
@@ -557,6 +557,36 @@ describe OllamaChat::Commands, protect_env: true do
557
557
  expect(chat).to receive(:confirm?).and_return false
558
558
  expect(chat.handle_input("/conversation compact")).to eq :next
559
559
  end
560
+
561
+ it 'returns :next when input is "/conversation summarize"' do
562
+ expect(chat).to receive(:summarize_conversation)
563
+ .with(save: false, sentence: false)
564
+ expect(chat.handle_input("/conversation summarize")).to eq :next
565
+ end
566
+
567
+ it 'returns :next when input is "/conversation summarize -s"' do
568
+ expect(chat).to receive(:summarize_conversation)
569
+ .with(save: false, sentence: 1)
570
+ expect(chat.handle_input("/conversation summarize -s")).to eq :next
571
+ end
572
+
573
+ it 'returns :next when input is "/conversation summarize -f"' do
574
+ expect(chat).to receive(:summarize_conversation)
575
+ .with(save: 1, sentence: false)
576
+ expect(chat.handle_input("/conversation summarize -f")).to eq :next
577
+ end
578
+
579
+ it 'returns :next when input is "/conversation report"' do
580
+ expect(chat).to receive(:report_conversation)
581
+ .with(save: false)
582
+ expect(chat.handle_input("/conversation report")).to eq :next
583
+ end
584
+
585
+ it 'returns :next when input is "/conversation report -f"' do
586
+ expect(chat).to receive(:report_conversation).
587
+ with(save: 1)
588
+ expect(chat.handle_input("/conversation report -f")).to eq :next
589
+ end
560
590
  end
561
591
 
562
592
  describe 'tools' do
@@ -226,9 +226,10 @@ describe OllamaChat::Compaction do
226
226
  )
227
227
  # last line is the directive's final sentence
228
228
  expect(captured)
229
- .to match(/details\s+not\s+present\s+in\s+the\s+groups\s+above./m)
229
+ .to match(/details\s+not\s+present\s+in\s+the\s+groups\s+or\s+previous\s+summary\s+above./m)
230
230
  # directive comes after content
231
- expect(captured.index('Summarize the conversation'))
231
+ expect(captured).to match(/is\s+a\s+draft/)
232
+ expect(captured.index(/is\s+a\s+draft/))
232
233
  .to be > captured.index('[user]')
233
234
  end
234
235
 
@@ -253,7 +254,7 @@ describe OllamaChat::Compaction do
253
254
  expect(content).to include 'tool_calls:'
254
255
  expect(content).to include '"uuid":"aaaa"'
255
256
  expect(content).to include '"uuid":"bbbb"'
256
- expect(content).to end_with 'with the group UUID in parentheses.'
257
+ expect(content).to end_with "with the group UUID in parentheses.\n"
257
258
  expect(tools).to eq entries
258
259
  end
259
260
 
@@ -261,7 +262,7 @@ describe OllamaChat::Compaction do
261
262
  content, tools = chat.assemble_summary('Just narrative.', [])
262
263
  expect(content).to start_with 'Just narrative.'
263
264
  expect(content).to include 'tool_calls:'
264
- expect(content).to end_with 'with the group UUID in parentheses.'
265
+ expect(content).to end_with "with the group UUID in parentheses.\n"
265
266
  expect(tools).to eq []
266
267
  end
267
268
 
@@ -8,7 +8,7 @@ describe OllamaChat::Conversation do
8
8
  describe '#save_conversation' do
9
9
  it 'saves a new conversation (clean: false) and confirms' do
10
10
  expect(chat.messages).to receive(:save_conversation)
11
- .with('./new_chat.jsonl', messages: chat.messages.messages)
11
+ .with(Pathname.new('./new_chat.jsonl'), messages: chat.messages.messages)
12
12
  .and_return(true)
13
13
  expect(STDOUT).to receive(:puts)
14
14
  .with('Saved conversation to "./new_chat.jsonl".')
@@ -17,7 +17,7 @@ describe OllamaChat::Conversation do
17
17
 
18
18
  it 'saves a new conversation (clean: true) using cleaned messages' do
19
19
  expect(chat.messages).to receive(:save_conversation)
20
- .with('./new_chat.jsonl', messages: chat.messages.clean_messages)
20
+ .with(Pathname.new('./new_chat.jsonl'), messages: chat.messages.clean_messages)
21
21
  .and_return(true)
22
22
  expect(STDOUT).to receive(:puts)
23
23
  .with('Saved conversation to "./new_chat.jsonl".')
@@ -56,7 +56,7 @@ describe OllamaChat::Conversation do
56
56
  .and_return(false)
57
57
  expect(chat.messages).not_to receive(:save_conversation)
58
58
  expect(STDOUT).not_to receive(:puts)
59
- expect(STDERR).not_to receive(:puts)
59
+ expect(STDERR).to receive(:puts).with('File not written!')
60
60
  chat.save_conversation(tmpfile, clean: false)
61
61
  ensure
62
62
  FileUtils.rm_f(tmpfile)
@@ -316,6 +316,92 @@ describe OllamaChat::SessionManagement do
316
316
  end
317
317
  end
318
318
 
319
+ describe '#summarize_conversation' do
320
+ it 'displays summary in pager when not saving' do
321
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
322
+ expect(chat).to receive(:summarize_session).and_return('# Summary content')
323
+ expect(chat).to receive(:use_pager)
324
+ chat.summarize_conversation
325
+ end
326
+
327
+ it 'reports nothing when no messages' do
328
+ expect(chat).to receive(:summarize_session).and_return(nil)
329
+ expect(STDERR).to receive(:puts).with('Nothing to summarize!')
330
+ chat.summarize_conversation
331
+ end
332
+
333
+ it 'saves summary to file when save: true' do
334
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
335
+ tmpfile = Pathname.new(File.join(Dir.tmpdir, "summ_#{$$}_test.md"))
336
+ expect(chat).to receive(:ask_for_filename?).and_return(tmpfile)
337
+ expect(chat).to receive(:should_overwrite?).and_return(true)
338
+ expect(chat).to receive(:summarize_session).and_return('# Summary content')
339
+ expect(STDOUT).to receive(:puts).with('File successfully written.')
340
+ chat.summarize_conversation(save: true)
341
+ expect(tmpfile.exist?).to be_truthy
342
+ expect(tmpfile.read).to include('Summary content')
343
+ ensure
344
+ FileUtils.rm_f(tmpfile)
345
+ end
346
+ end
347
+
348
+ describe '#report_session' do
349
+ it 'generates a report from compacted messages' do
350
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello world')
351
+ expect(chat).to receive(:sender_name_displayed).and_return('User')
352
+ expect(chat).to receive(:choose_prompt).and_return(double(to_s: '%{content}'))
353
+ expect(chat).to receive(:generate).and_return('Generated report')
354
+
355
+ results = []
356
+ result = chat.report_session { |c| results << c }
357
+ expect(result).to eq('Generated report')
358
+ expect(results).to eq(['Generated report'])
359
+ end
360
+
361
+ it 'returns nil when no messages have content' do
362
+ expect(chat.messages).to receive(:compacted_messages).and_return([])
363
+ expect(chat.report_session).to be_nil
364
+ end
365
+
366
+ it 'uses named prompt when name is provided' do
367
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
368
+ expect(chat).to receive(:sender_name_displayed).and_return('User')
369
+ expect(chat).to receive(:prompt).with('coding_brief', context: 'session')
370
+ .and_return(double(to_s: '%{content}'))
371
+ expect(chat).to receive(:generate).and_return('Brief')
372
+ expect(chat.report_session(name: 'coding_brief')).to eq('Brief')
373
+ end
374
+ end
375
+
376
+ describe '#report_conversation' do
377
+ it 'displays report in pager when not saving' do
378
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
379
+ expect(chat).to receive(:report_session).and_return('# Report content')
380
+ expect(chat).to receive(:use_pager)
381
+ chat.report_conversation
382
+ end
383
+
384
+ it 'reports nothing when no content' do
385
+ expect(chat).to receive(:report_session).and_return(nil)
386
+ expect(STDERR).to receive(:puts).with('Nothing to report!')
387
+ chat.report_conversation
388
+ end
389
+
390
+ it 'saves report to file when save: true' do
391
+ chat.messages << OllamaChat::Message.new(role: 'user', content: 'hello')
392
+ tmpfile = Pathname.new(File.join(Dir.tmpdir, "report_#{$$}_test.md"))
393
+ expect(chat).to receive(:ask_for_filename?).and_return(tmpfile)
394
+ expect(chat).to receive(:should_overwrite?).and_return(true)
395
+ expect(chat).to receive(:report_session).and_return('# Report content')
396
+ expect(STDOUT).to receive(:puts).with('File successfully written.')
397
+ chat.report_conversation(save: true)
398
+ expect(tmpfile.exist?).to be_truthy
399
+ expect(tmpfile.read).to include('Report content')
400
+ ensure
401
+ FileUtils.rm_f(tmpfile)
402
+ end
403
+ end
404
+
319
405
  describe '#change_session' do
320
406
  it 'switches to a different session and updates state' do
321
407
  new_s = chat.new_session.tap { |s| s.name = 'change_to_me'; s.save }
@@ -97,10 +97,19 @@ describe OllamaChat::ToolCalling do
97
97
 
98
98
  describe '#list_tools' do
99
99
  it 'prints a formatted list of tools with their status' do
100
- chat.session.tools_default_enabled = { 'read_file' => true, 'write_file' => false }
100
+ chat.session.tools_default_enabled = {
101
+ 'read_file' => true, 'write_file' => false
102
+ }
103
+
104
+ output_io = StringIO.new
105
+ expect(chat).to receive(:use_pager).and_yield(output_io)
106
+ expect(chat).to receive(:tools_support).
107
+ and_return(double(show: true))
108
+
109
+ chat.list_tools
101
110
 
102
- # We expect to see for read_file and for write_file
103
- expect { chat.list_tools }.to output(/✓ .*read_file.*☐ .*write_file/m).to_stdout
111
+ # We expect to see for read_file and for write_file
112
+ expect(output_io.string).to match(/✅ .*read_file.*⛔ .*write_file/m)
104
113
  end
105
114
  end
106
115
 
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.115
4
+ version: 0.0.116
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank