ollama_chat 0.0.117 → 0.0.119

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: cbe0a034dee8079ea14e573b0e98624ebaa3311baaf60e1bd666d7f7b0d61dda
4
- data.tar.gz: 2a2b0c1b00ce5db2fa4b20660d8cda86669f1bac50e62352a07953e0c327d863
3
+ metadata.gz: a7b876b681130c1ff2ec672460bb04aa85ed095e9358dbdaa62d744f82d68720
4
+ data.tar.gz: 3f85175d15231880d45e8f192f9f9af47c861cb8f1475d2fe653309ca173ab1c
5
5
  SHA512:
6
- metadata.gz: f187ae33f48c198b1aa15d680b5d7dd45e96c8ba7bcb71bf80adf5d0cb049a766d79d57d8bad32175363c490cfe94a94908b1eba4a1ba28a8b0e9896065079ed
7
- data.tar.gz: 0d61b926d6e7018d5f21d52ed06d36e81ea859dc5f288a0247834def14245813f1306bf444baaf715c8ec37df20ea64d97f03279045e34eb669a29c9facadb23
6
+ metadata.gz: 9cf60761b6ce28cb22873b30808d3398ffcefc9e9991fcf781621b4db8a8fed5fd6cca21de046d8bf2851dd60a7529cfa162125ca4ee567e32c09042f4dc4e81
7
+ data.tar.gz: a2f1e6210141a8f00c4b9428453c49e4e1673e51521f788dcec694cd2c2c9963542bd491618ec8d1a4bb3ad5788588ca3d2c7f51ed3b67b977eaac1b4a59115a
data/CHANGES.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-14 v0.0.119
4
+
5
+ * **Word-boundary fuzzy matching in chooser**
6
+ * Pass `/\b/` as the word-boundary argument to `matcher.similar` in
7
+ `chooser.rb` to filter out partial-word hits, improving ranked results
8
+ for model names containing `/`, `-`, `:`, and `.` separators.
9
+
10
+ * **Parallel embedding via bounded thread pool**
11
+ * Add `concurrency: 3` under `embedding:` in `default_config.yml`.
12
+ * Add `bulk_embed_sources` to `source_fetching.rb` using a
13
+ `Tins::Limited` bounded thread pool with a `Mutex`-guarded results
14
+ array.
15
+ * Change `embed` to accept a `prompt:` kwarg (defaults to
16
+ `prompt(:embed)`) so worker threads share a single resolved template
17
+ instead of each hitting Sequel/SQLite.
18
+ * Replace `STDOUT.puts` with `infobar.puts` in `embed_source` to prevent
19
+ raw output from clobbering the progress bar in concurrent workers.
20
+ * Rewrite the `-p` pattern path in `commands.rb` embedding to build a
21
+ `{source => tags}` Hash and call `bulk_embed_sources`.
22
+ * Rewrite `update_collection` in `rag_handling.rb` to collect both
23
+ modified and new sources into a single Hash and issue one
24
+ `bulk_embed_sources` call instead of a sequential `embed` loop.
25
+ * Update `commands_spec.rb` to stub `bulk_embed_sources` returning an
26
+ array.
27
+
28
+ * **Speed up embedding generation**
29
+ * Implement batch processing in the ollama server.
30
+
31
+ * **Dependency updates**
32
+ * Update `documentrix` minimum version to **0.7.0** in `Rakefile` and
33
+ `ollama_chat.gemspec` to support improved embedding speed.
34
+
35
+ * **Improve session switching and duplication robustness**
36
+ * In `duplicate_session`, replaced defensive `session.lock?` check with a
37
+ direct `session.lock` call, since a freshly duplicated session is never
38
+ yet locked.
39
+ * In `change_session`, added `except_id: session.id` to `choose_session`
40
+ to prevent re-selecting the current session from the chooser.
41
+ * Track `previous_session` as a full object instead of just an ID,
42
+ enabling restoration on lock failure.
43
+ * On lock failure, restore `@session` to the previous session, re-acquire
44
+ its lock, and reset `name` to `??` so the next iteration opens the
45
+ interactive picker instead of retrying the same locked session.
46
+ * Log `previous_session_id` correctly in the "Session changed" info
47
+ message.
48
+
49
+ * **Formatting fix**
50
+ * Add space before `\`.
51
+
52
+ * **Fix `context_spook` returning `nil` instead of context**
53
+ * Reordered `STDOUT.puts` and `ctx.send("to_#{format.downcase}")` in
54
+ `context_spook` so the serialized context is the last expression in the
55
+ `if count > 0` branch, preventing `STDOUT.puts` from clobbering the
56
+ implicit return value.
57
+ * Added a regression spec in `input_content_spec.rb` that exercises the
58
+ `count > 0` path with a real file and `all: true`, asserting the return
59
+ value is a non-blank `String` containing the ingested file path.
60
+
61
+ ## 2026-09-05 v0.0.118
62
+
63
+ * Documented the upgrade workflow in the README, adding an "Upgrading"
64
+ section that covers the post-`gem update` flow: `/config diff` with
65
+ `:diffget`, reload, and `/prompt sync` for per-prompt drift resolution.
66
+ * Clarified that the local `config.yml` is a partial override requiring no
67
+ migration script.
68
+ * Improved the prompt drift nagging message in `app_state.rb#seed` by
69
+ replacing the previous prompt with a two-part message featuring a bold
70
+ `Tip:` line suggesting `/prompt sync` and a "Seen these changes?"
71
+ confirmation.
72
+ * Replaced literal `⚠️` emoji with `\u26A0\uFE0F` escapes for consistent
73
+ rendering and introduced `Term::ANSIColor` locally for the bold tip prefix.
74
+ * Added a `session.standup` template to `default_config.yml` defining a
75
+ six-section end-of-day document.
76
+ * Moved the compaction system prompt from `prompts.compaction.system` to
77
+ `prompts.system.compaction` and updated `compaction.rb` to call
78
+ `prompt(:compaction, context: 'system')`.
79
+ * Added `prompts.system.report`, a genre-neutral writer system prompt, and
80
+ updated `session_management.rb#report_session` to pass `system:` and
81
+ `think: true` to `generate` for report generation.
82
+ * Simplified `compaction_spec.rb` to resolve templates from
83
+ `chat.config.prompts.system.compaction` and added a `prompt(:report,
84
+ context: 'system')` expectation to `session_management_spec.rb`.
85
+ * Fixed the `get_url` summarizing policy by wrapping it in `Infobar.busy` and
86
+ `chat.generate` to ensure the LLM produces the summary.
87
+ * Added a `words` (integer) parameter to the `get_url` tool schema, forwarded
88
+ to `summarize_source` to control summary length.
89
+ * Changed `STDOUT.puts` to `infobar.puts` in `summarize_source` to prevent
90
+ garbling spinner output.
91
+ * Updated `get_url_spec.rb` to include `words: nil` in argument doubles,
92
+ reworked the summarizing test to expect `chat.generate`, and added a
93
+ `words: 50` forwarding test.
94
+ * Added `Infobar.busy` spinners with descriptive labels to LLM `generate`
95
+ calls in `personae_management.rb`, `prompt_management.rb`, and
96
+ `session_management.rb`.
97
+ * Modified `prompt_management.rb` to only call `edit_text(suggestions)` if
98
+ `suggestions.present?`.
99
+
3
100
  ## 2026-09-03 v0.0.117
4
101
 
5
102
  * Fixed `/favourite` subcommand completion rendering by replacing separate
data/README.md CHANGED
@@ -19,6 +19,39 @@ gem install ollama_chat
19
19
 
20
20
  in your terminal.
21
21
 
22
+ ## Upgrading
23
+
24
+ After `gem update ollama_chat`, the next launch will either
25
+ start cleanly or prompt you to fix the config:
26
+
27
+ 1. **Open the config diff.** Run `/config diff`, or wait for
28
+ the automatic `fix_config` prompt if the new version
29
+ introduced required keys. Either way, your `$DIFF_TOOL`
30
+ (default: `vimdiff`) opens with your local `config.yml`
31
+ on one side and the shipped `default_config.yml` on the
32
+ other.
33
+ 2. **Pull in the changes.** In vimdiff, use `:diffget` to
34
+ copy new or updated keys from the default into your local
35
+ file. Ignore keys you've intentionally overridden.
36
+ 3. **Reload.** Close the diff tool and answer `y` to the
37
+ "restart?" prompt.
38
+ 4. **Prompt drift.** If the new version changed any shipped
39
+ prompts, a fingerprint notice lists modified, added, and
40
+ removed prompts. To reconcile your stored prompts with
41
+ the new defaults, run `/prompt sync`. It diffs each
42
+ prompt against the shipped version and lets you decide
43
+ per prompt: adopt the new default, keep your local edit,
44
+ or review the change. After syncing, the fingerprint is
45
+ updated and the nagging stops.
46
+
47
+ If you simply want to dismiss the notice without
48
+ syncing, answer `y` to the boot prompt. The fingerprint
49
+ is stored and you won't be asked again.
50
+
51
+ Your local config is a partial override. Keys you don't
52
+ have fall through to the shipped defaults. No migration
53
+ script needed.
54
+
22
55
  ## Configuration
23
56
 
24
57
  ### Environment Variables
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.6.0'
43
+ dependency 'documentrix', '>= 0.7.0'
44
44
  dependency 'unix_socks', '>= 0.4'
45
45
  dependency 'rss', '~> 0.3'
46
46
  dependency 'term-ansicolor', '~> 1.11'
@@ -883,7 +883,8 @@ module OllamaChat::Commands
883
883
  if opts[?p]
884
884
  all = opts.fetch(?a, false)
885
885
  patterns = extract_patterns(arg)
886
- next provide_file_set_content(patterns, all:, skip_blank: true) { embed(_1, tags:) } || :next
886
+ sources = file_set_each(patterns, all:).map(&:to_s).to_h { [_1, tags] }
887
+ next bulk_embed_sources(sources) || :next
887
888
  elsif arg
888
889
  next embed(arg, tags:) || :next
889
890
  else
@@ -191,7 +191,7 @@ module OllamaChat::Compaction
191
191
  prompt = prompt(:summarize, context: 'compaction').to_s % {
192
192
  previous:, groups:
193
193
  }
194
- system = prompt(:system, context: 'compaction').to_s
194
+ system = prompt(:compaction, context: 'system').to_s
195
195
 
196
196
  es = OllamaChat::TokenEstimator.estimate(prompt)
197
197
  log(:info, "Compaction: sending prompt " \
@@ -73,19 +73,20 @@ class OllamaChat::Database::Models::AppState < Sequel::Model(OllamaChat::DB)
73
73
  removed = old.keys - hashes.keys
74
74
  changed = hashes.select { |k, v| old[k] && old[k] != v }.keys
75
75
 
76
- STDERR.puts "⚠️ Shipped prompts changed since last boot:"
76
+ STDERR.puts "\u26A0\uFE0F Shipped prompts changed since last boot:"
77
77
  changed.each { |k| STDERR.puts " ~ #{k} (modified)" }
78
78
  added.each { |k| STDERR.puts " + #{k} (new)" }
79
79
  removed.each { |k| STDERR.puts " - #{k} (removed)" }
80
80
  elsif !OllamaChat.test_mode?
81
- STDERR.puts '⚠️ First run — storing prompt fingerprints.'
81
+ STDERR.puts "\u26A0\uFE0F First run — storing prompt fingerprints."
82
82
  store_fingerprint(fingerprint, hashes)
83
83
  return true
84
84
  end
85
85
 
86
+ c = Term::ANSIColor
86
87
  prompt = <<~EOT.chomp << ' '
87
- Keep local prompts (stop nagging)?
88
- Consider adopting via /prompt sync or just ignore? (y/n) %s
88
+ #{c.bold{'Tip'}}: /prompt sync adopts shipped defaults into local config.
89
+ Seen these changes? (y = stop nagging, n = ask again next boot) %s
89
90
  EOT
90
91
  if OllamaChat.test_mode? || chat.confirm?(prompt:, yes: /\Ay/i, timeout: 5)
91
92
  store_fingerprint(fingerprint, hashes)
@@ -106,8 +106,8 @@ module OllamaChat::InputContent
106
106
  end
107
107
  end
108
108
  if count > 0
109
- ctx.send("to_#{format.downcase}")
110
109
  STDOUT.puts "✅ Ingesting context now."
110
+ ctx.send("to_#{format.downcase}")
111
111
  else
112
112
  STDERR.puts "❌ No files in context. ⇨ Cancelled."
113
113
  end
@@ -190,12 +190,40 @@ prompts:
190
190
 
191
191
  Write a narrative retelling of the roleplay session
192
192
  above. Your story MUST:
193
+
193
194
  - Capture the emotional arc and key plot beats.
194
195
  - Stay in the established setting and character voices.
195
196
  - Use vivid, evocative prose; show, don't tell.
196
197
  - Highlight turning points, revelations, and character
197
198
  growth.
198
199
  - Read it like a short story chapter, not a log.
200
+ standup: |
201
+ %{content}
202
+
203
+ Write a document for an end-of-session snapshot. It captures what was
204
+ accomplished, which decisions were made and why, and what happens next —
205
+ the same alignment a Scrum standup provides, but written down so no
206
+ context is lost between sessions.
207
+
208
+ Your document MUST contain these exact sections:
209
+
210
+ 1. ## Progress Log — bulleted list of completed tasks with relative file
211
+ paths. 2. ## External References — all URLs, API docs, or GitHub links
212
+ discussed during the session. 3. ## Current Mental Model — current focus,
213
+ existing blockers, and the immediate next step. 4. ## Technical Decision
214
+ Log — rationale for chosen implementations vs. rejected alternatives
215
+ (table). 5. ## Pending Roadmap — Critical → Important → Future Polish. 6.
216
+ ## Quick-Start Pointers — 2–3 files or links the next session must read
217
+ #first.
218
+
219
+ Constraints:
220
+ - Use relative Markdown paths for file references.
221
+ - Document *why* decisions were made, not just *what*.
222
+ - Do not restate what the conversation records verbatim.
223
+ - Professional, precise, technical tone.
224
+
225
+ Output only the standup document. No preamble, no
226
+ postamble, no meta-commentary.
199
227
  suggest:
200
228
  coding: |
201
229
  ## Context
@@ -366,12 +394,6 @@ prompts:
366
394
  Output only the 9 suggestions as plain text, each separated by one empty line.
367
395
  No other text.
368
396
  compaction:
369
- system: |
370
- You are a context summarizer for a chat application.
371
- Your sole task is to produce a concise, factual narrative
372
- summary of the conversation provided. Be precise, omit
373
- speculation, and follow the formatting instructions in the
374
- user prompt exactly. Output only the summary text.
375
397
  summarize: |
376
398
  previous_summary:
377
399
  %{previous}
@@ -408,6 +430,20 @@ prompts:
408
430
 
409
431
  %{runtime_info}
410
432
  assistant: You are a helpful assistant.
433
+ compaction: |
434
+ You are a context summarizer for a chat application.
435
+ Your sole task is to produce a concise, factual narrative
436
+ summary of the conversation provided. Be precise, omit
437
+ speculation, and follow the formatting instructions in the
438
+ user prompt exactly. Output only the summary text.
439
+ report: |
440
+ You are a writer. The user prompt below
441
+ specifies the document to produce: its
442
+ structure, tone, and constraints. Follow
443
+ it exactly. Output only the final document
444
+ — no preamble, no postamble, no commentary,
445
+ no reference to yourself or the act of
446
+ writing.
411
447
  voice:
412
448
  handler: OllamaChat::Say
413
449
  enabled: false
@@ -442,10 +478,12 @@ embedding:
442
478
  name: snowflake-arctic-embed:137m
443
479
  embedding_length: 768
444
480
  context_length: 2048
445
- options: {}
481
+ options:
482
+ num_batch: 2048
446
483
  # Retrieval prompt template:
447
484
  prompt: 'Represent this sentence for searching relevant passages: %s'
448
- batch_size: 100
485
+ batch_size: 500
486
+ concurrency: 3
449
487
  database_filename: null # ':memory:'
450
488
  collection: <%= OC::OLLAMA::CHAT::COLLECTION %>
451
489
  found_texts_size: 16384
@@ -597,12 +597,18 @@ module OllamaChat::PersonaeManagement
597
597
  # @param persona_name [String] the name of the character to replace {{char}} with
598
598
  # @return [String] the resulting structured Markdown persona profile
599
599
  def convert_json_character_to_markdown(character, persona_name)
600
- generate(
601
- prompt: prompt(:persona_architect).to_s % {
602
- character:,
603
- persona_template: prompt(:persona).to_s
604
- }
605
- ).gsub(/{{user}}/i, '%{user}').gsub(/{{char}}/i, persona_name)
600
+ Infobar.busy(
601
+ label: 'Converting character…',
602
+ frames: :braille7,
603
+ output: STDOUT,
604
+ ) do
605
+ generate(
606
+ prompt: prompt(:persona_architect).to_s % {
607
+ character:,
608
+ persona_template: prompt(:persona).to_s
609
+ }
610
+ ).gsub(/{{user}}/i, '%{user}').gsub(/{{char}}/i, persona_name)
611
+ end
606
612
  end
607
613
 
608
614
  # Interactively exports a persona profile to a specified file.
@@ -346,10 +346,16 @@ module OllamaChat::PromptManagement
346
346
  full_prompt = template % { history:, instruction: }
347
347
 
348
348
  # Execute a silent chat oneshot call (doesn't add to history)
349
- suggestions = generate(prompt: full_prompt).full? or return
349
+ suggestions = Infobar.busy(
350
+ label: 'Creating suggestions…',
351
+ frames: :braille7,
352
+ output: STDOUT,
353
+ ) do
354
+ generate(prompt: full_prompt)
355
+ end
350
356
 
351
357
  # Pass the AI's suggestions through the editor for final refinement
352
- edit_text(suggestions)
358
+ edit_text(suggestions) if suggestions.present?
353
359
  end
354
360
 
355
361
  # Lists all prompt templates in the database, indicating which are defaults
@@ -426,7 +432,7 @@ module OllamaChat::PromptManagement
426
432
  end
427
433
 
428
434
  if orphans.any?
429
- STDOUT.puts "\n#{orphans.count} orphaned prompt(s) "\
435
+ STDOUT.puts "\n#{orphans.count} orphaned prompt(s) " \
430
436
  "(no longer in default config):\n"
431
437
  orphans.each { |p| STDOUT.puts " • #{bold{p.name}} in #{italic{p.context}}" }
432
438
  STDOUT.puts
@@ -184,12 +184,13 @@ module OllamaChat::RAGHandling
184
184
  #
185
185
  # @return [String] a newline-separated string of embedding result messages.
186
186
  def update_collection(collection)
187
+ results = []
187
188
  switch_collection(collection) do
188
189
  unless col = database_collection?(collection)
189
190
  STDERR.puts "❌ Collection #{collection.inspect} not found in database."
190
- return
191
+ return ''
191
192
  end
192
- results = []
193
+ sources = {}
193
194
  seen = {}
194
195
  @documents.each_record do |record|
195
196
  source = @documents.normalize_source(record.source) or next
@@ -199,24 +200,20 @@ module OllamaChat::RAGHandling
199
200
  infobar.puts "Source #{source.to_s.inspect} is unmodified. => Skipping."
200
201
  next
201
202
  end
202
- tags = record.tags_set
203
+ sources[source] = record.tags_set
203
204
  @documents.source_remove(source)
204
- r = embed(source, tags:) or next
205
- results << r
206
205
  end
207
206
 
208
207
  if patterns = col.patterns.full?
209
- all_file_set(patterns).each do |file|
210
- seen[file.to_s] and next
211
- seen[file.to_s] = true
212
- r = embed(file.to_s, tags: []) or next
213
- results << r
214
- end
208
+ new_sources = all_file_set(patterns).map(&:to_s).reject { seen.key?(_1) }
209
+ new_sources.each { seen[_1] = true }
210
+ new_sources.each { sources[_1] = [] }
215
211
  end
216
212
 
213
+ results.concat(bulk_embed_sources(sources))
217
214
  log(:info, "Collection updated", data: { collection:, sources_updated: results.size })
218
- results * "\n"
219
215
  end
216
+ results * ?\n
220
217
  end
221
218
 
222
219
  # Extracts and normalizes file patterns from a shell-style string.
@@ -219,8 +219,7 @@ module OllamaChat::SessionManagement
219
219
  @session = session.duplicate
220
220
  set_previous_session_on_change(old_session.id)
221
221
  session.update(name:)
222
- session.lock? or raise OllamaChat::OllamaChatError,
223
- "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
222
+ session.lock
224
223
  confirm?(
225
224
  prompt: "🔔 Clear messages of duplicated session? (y/n) ",
226
225
  yes: /\Ay/i
@@ -444,7 +443,15 @@ module OllamaChat::SessionManagement
444
443
  end
445
444
  template or return
446
445
 
447
- generate(prompt: template.to_s % { content: })
446
+ system = prompt(:report, context: 'system').to_s
447
+
448
+ Infobar.busy(
449
+ label: 'Generating session report…',
450
+ frames: :braille7,
451
+ output: STDOUT,
452
+ ) do
453
+ generate(system:, prompt: template.to_s % { content: }, think: true)
454
+ end
448
455
  end
449
456
 
450
457
  # Generates a report and displays or saves the result.
@@ -485,11 +492,17 @@ module OllamaChat::SessionManagement
485
492
  c << "%s: %s\n\n" % [ sender_name, message.content ]
486
493
  end
487
494
  prompt = prompt(:session_title).to_s % { length:, content: }
488
- generate(prompt:).full? do |name|
489
- name = name.
490
- gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
491
- gsub(/\s+/, ' ')
492
- Kramdown::ANSI::Width.truncate(name, length:)
495
+ Infobar.busy(
496
+ label: 'Naming session…',
497
+ frames: :braille7,
498
+ output: STDOUT,
499
+ ) do
500
+ generate(prompt:).full? do |name|
501
+ name = name.
502
+ gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
503
+ gsub(/\s+/, ' ')
504
+ Kramdown::ANSI::Width.truncate(name, length:)
505
+ end
493
506
  end
494
507
  end
495
508
 
@@ -523,9 +536,9 @@ module OllamaChat::SessionManagement
523
536
  # @param name [String] the name or ID of the session to switch to
524
537
  def change_session(name)
525
538
  name.full? or name = ??
526
- previous_session_id = nil
539
+ previous_session = nil
527
540
  loop do
528
- if chosen_session = choose_session(name, allow_new: true)
541
+ if chosen_session = choose_session(name, except_id: session.id, allow_new: true)
529
542
  if chosen_session.nil? || chosen_session == session
530
543
  confirm?(
531
544
  prompt: "\n⏎ Same session chosen, Press any key to continue (%s). ",
@@ -534,7 +547,7 @@ module OllamaChat::SessionManagement
534
547
  break
535
548
  end
536
549
  session_close
537
- previous_session_id = session.id
550
+ previous_session = session
538
551
  @session = chosen_session
539
552
  if session.lock?
540
553
  messages.read_conversation_jsonl(session.messages.to_s)
@@ -549,10 +562,13 @@ module OllamaChat::SessionManagement
549
562
  set_default_persona_name(session.default_persona_name.full? || :none)
550
563
  set_current_system_prompt(session.current_system_prompt.full? || 'default')
551
564
  session_apply
552
- log(:info, "Session changed", data: { session_id: session.id, name: session.name, previous_session_id: })
565
+ log(:info, "Session changed", data: { session_id: session.id, name: session.name, previous_session_id: previous_session&.id })
553
566
  info_session
554
567
  break
555
568
  else
569
+ @session = previous_session
570
+ @session.lock
571
+ name = ??
556
572
  confirm?(
557
573
  prompt: "\n⏎ Session locked: could not switch, Press any key to continue (%s). ",
558
574
  timeout: 3
@@ -565,7 +581,7 @@ module OllamaChat::SessionManagement
565
581
  end
566
582
  end
567
583
  ensure
568
- set_previous_session_on_change(previous_session_id)
584
+ set_previous_session_on_change(previous_session&.id)
569
585
  session_sync
570
586
  end
571
587
 
@@ -144,7 +144,7 @@ module OllamaChat::SourceFetching
144
144
  # @param words [Integer, nil] The target number of words for the summary (defaults to 100)
145
145
  # @return [String, nil] The formatted summary message or nil if content is empty or cannot be processed
146
146
  def summarize_source(source_io, source, words: nil)
147
- STDOUT.puts "Summarizing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
147
+ infobar.puts "Summarizing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
148
148
  log(:info, "Source summarized", data: { source: source.to_s, content_type: source_io&.content_type, words: })
149
149
  words = words.to_i
150
150
  words < 1 and words = 100
@@ -185,7 +185,7 @@ module OllamaChat::SourceFetching
185
185
  def embed_source(source_io, source, tags: [], count: nil)
186
186
  @embedding.on? or return parse_source(source_io)
187
187
  unless @documents.source_modified?(source)
188
- STDOUT.puts "Source #{source.to_s.inspect} already up-to-date. => Skipping."
188
+ infobar.puts "Source #{source.to_s.inspect} already up-to-date. => Skipping."
189
189
  log(:info, "Source up-to-date", data: { source: source.to_s })
190
190
  return
191
191
  end
@@ -225,9 +225,9 @@ module OllamaChat::SourceFetching
225
225
  chunks: inputs.size
226
226
  })
227
227
  if count
228
- STDOUT.puts '%u. %s' % [ count, m ]
228
+ infobar.puts '%u. %s' % [ count, m ]
229
229
  else
230
- STDOUT.puts m
230
+ infobar.puts m
231
231
  end
232
232
  source = source.to_s
233
233
  command = false
@@ -253,10 +253,14 @@ module OllamaChat::SourceFetching
253
253
  #
254
254
  # @param source [String] The source identifier which can be a command, URL,
255
255
  # or file path
256
+ # @param tags [Array<String>] Tags to apply to the embedded source
257
+ # @param prompt [#to_s] The prompt template to format the result with
258
+ # (defaults to `prompt(:embed)`)
256
259
  #
257
260
  # @return [String, nil] The formatted embedding result or summary message, or
258
261
  # nil if the operation fails
259
- def embed(source, tags: [])
262
+ def embed(source, tags: [], prompt: prompt(:embed))
263
+ prompt = prompt.to_s
260
264
  @embedding.on? or return
261
265
  fetch_source(source) do |source_io|
262
266
  content = parse_source(source_io)
@@ -264,7 +268,31 @@ module OllamaChat::SourceFetching
264
268
  source_io.rewind
265
269
  embed_source(source_io, source, tags:) or return
266
270
  end
267
- prompt(:embed).to_s % { source:, collection: }
271
+ prompt.to_s % { source:, collection: }
272
+ end
273
+
274
+ # Embeds multiple sources concurrently using a bounded thread pool.
275
+ #
276
+ # @param sources_tags [Hash{String => Array}] mapping of source identifier
277
+ # to the tags array to apply to that source
278
+ # @return [Array<String>] results from each successful embed call
279
+ def bulk_embed_sources(sources_tags)
280
+ return sources_tags.map { |s, tags| embed(s, tags:) }.compact if
281
+ sources_tags.size <= 1 or config.embedding.concurrency <= 1
282
+
283
+ results = []
284
+ mutex = Mutex.new
285
+ prompt = prompt(:embed)
286
+ Tins::Limited.new(config.embedding.concurrency, name: 'embed').process do |l|
287
+ sources_tags.each do |s, tags|
288
+ l.execute do
289
+ r = embed(s, prompt:, tags:)
290
+ mutex.synchronize { results << r } if r
291
+ end
292
+ end
293
+ l.stop
294
+ end
295
+ results
268
296
  end
269
297
 
270
298
  private
@@ -52,6 +52,13 @@ class OllamaChat::Tools::GetURL
52
52
  - 'summarizing': Returns a condensed summary of the content.
53
53
  EOT
54
54
  ),
55
+ words: Tool::Function::Parameters::Property.new(
56
+ type: 'integer',
57
+ description: <<~EOT,
58
+ Target word count for the summary (only applies when
59
+ document_policy is 'summarizing'). Defaults to 100.
60
+ EOT
61
+ ),
55
62
  },
56
63
  required: %w[url]
57
64
  )
@@ -75,6 +82,7 @@ class OllamaChat::Tools::GetURL
75
82
  args = tool_call.function.arguments
76
83
  url = args.url.to_s
77
84
  document_policy = args.document_policy.full? || 'ignoring'
85
+ words = args.words.to_i
78
86
 
79
87
  allowed_schemes = Array(config.tools.functions.get_url.schemes?).map(&:to_s)
80
88
 
@@ -99,7 +107,13 @@ class OllamaChat::Tools::GetURL
99
107
  when 'embedding'
100
108
  content = chat.embed_source(source_io, source)
101
109
  when 'summarizing'
102
- content = chat.summarize_source(source_io, source)
110
+ content = Infobar.busy(
111
+ label: 'Summarizing…',
112
+ frames: :braille7,
113
+ output: STDOUT,
114
+ ) do
115
+ chat.generate(prompt: chat.summarize_source(source_io, source, words:))
116
+ end
103
117
  else
104
118
  message = "Invalid document policy #{document_policy.inspect} used."
105
119
  end
@@ -51,7 +51,7 @@ module OllamaChat::Utils::Chooser
51
51
  matches = entries.map { |n|
52
52
  [
53
53
  n,
54
- -matcher.similar(n.ask_and_send_or_self(:value).to_s.downcase)
54
+ -matcher.similar(n.ask_and_send_or_self(:value).to_s.downcase, /\b/)
55
55
  ]
56
56
  }.select { |_, s| s < 0 }.sort_by(&:last).map(&:first)
57
57
  matches.empty? and matches = entries
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.117'
3
+ VERSION = '0.0.119'
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.117 ruby lib
2
+ # stub: ollama_chat 0.0.119 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.117".freeze
6
+ s.version = "0.0.119".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]
@@ -35,7 +35,7 @@ Gem::Specification.new do |s|
35
35
  s.add_development_dependency(%q<utils>.freeze, [">= 0".freeze])
36
36
  s.add_runtime_dependency(%q<excon>.freeze, ["~> 1.0".freeze])
37
37
  s.add_runtime_dependency(%q<ollama-ruby>.freeze, ["~> 1.23".freeze])
38
- s.add_runtime_dependency(%q<documentrix>.freeze, [">= 0.6.0".freeze])
38
+ s.add_runtime_dependency(%q<documentrix>.freeze, [">= 0.7.0".freeze])
39
39
  s.add_runtime_dependency(%q<unix_socks>.freeze, [">= 0.4".freeze])
40
40
  s.add_runtime_dependency(%q<rss>.freeze, ["~> 0.3".freeze])
41
41
  s.add_runtime_dependency(%q<term-ansicolor>.freeze, ["~> 1.11".freeze])
@@ -411,11 +411,11 @@ describe OllamaChat::Commands, protect_env: true do
411
411
  end
412
412
 
413
413
  it 'returns "success" when input is "/input embedding -p (.+)"' do
414
- expect(chat).to receive(:embed).
415
- with(asset_pathname('example.rb'), tags: nil).
416
- and_return 'success'
417
- expect(chat.handle_input("/input embedding -a -p #{asset('*.rb')}")).
418
- to match(/success/)
414
+ expect(chat).to receive(:bulk_embed_sources).
415
+ with(anything).
416
+ and_return [ 'success' ]
417
+ expect(chat.handle_input("/input embedding -a -p #{asset('*.rb')}"))
418
+ .to eq [ 'success' ]
419
419
  end
420
420
 
421
421
  it 'returns :next when input is "/input embedding"' do
@@ -9,31 +9,11 @@ describe OllamaChat::Compaction do
9
9
 
10
10
  connect_to_ollama_server
11
11
 
12
- let :summarize_template do
13
- chat.config.prompts.compaction.summarize
14
- end
15
-
16
12
  let :system_template do
17
- chat.config.prompts.compaction.system
18
- end
19
-
20
- let :assemble_template do
21
- chat.config.prompts.compaction.assemble
13
+ chat.config.prompts.system.compaction
22
14
  end
23
15
 
24
16
  before do
25
- allow(chat).to receive(:prompt)
26
- .with(:summarize, context: 'compaction')
27
- .and_return(summarize_template)
28
-
29
- allow(chat).to receive(:prompt)
30
- .with(:system, context: 'compaction')
31
- .and_return(system_template)
32
-
33
- allow(chat).to receive(:prompt)
34
- .with(:assemble, context: 'compaction')
35
- .and_return(assemble_template)
36
-
37
17
  allow(chat).to receive(:log)
38
18
  end
39
19
 
@@ -97,5 +97,15 @@ describe OllamaChat::InputContent do
97
97
  and_return(nil)
98
98
  expect(chat.context_spook(nil)).to be_nil
99
99
  end
100
+
101
+ it 'returns the serialized context instead of nil' do
102
+ patterns = ['spec/assets/example.rb']
103
+ expect(STDOUT).to receive(:puts).with(/Ingesting context now/)
104
+ result = chat.context_spook(patterns, all: true)
105
+ expect(result).to be_a(String)
106
+ json = JSON.parse(result)
107
+ expected = Pathname.new('spec/assets/example.rb').expand_path.to_path
108
+ expect(json['files'].keys).to include(expected)
109
+ end
100
110
  end
101
111
  end
@@ -366,6 +366,8 @@ describe OllamaChat::SessionManagement do
366
366
  expect(chat).to receive(:sender_name_displayed).and_return('User')
367
367
  expect(chat).to receive(:prompt).with('coding_brief', context: 'session')
368
368
  .and_return(double(to_s: '%{content}'))
369
+ expect(chat).to receive(:prompt).with(:report, context: 'system')
370
+ .and_return(double(to_s: '%{content}'))
369
371
  expect(chat).to receive(:generate).and_return('Brief')
370
372
  expect(chat.report_session(name: 'coding_brief')).to eq('Brief')
371
373
  end
@@ -26,7 +26,8 @@ describe OllamaChat::Tools::GetURL do
26
26
  name: 'get_url',
27
27
  arguments: double(
28
28
  url: 'https://www.example.com/foo',
29
- document_policy: nil
29
+ document_policy: nil,
30
+ words: nil
30
31
  )
31
32
  )
32
33
  )
@@ -55,7 +56,8 @@ describe OllamaChat::Tools::GetURL do
55
56
  name: 'get_url',
56
57
  arguments: double(
57
58
  url:,
58
- document_policy: nil
59
+ document_policy: nil,
60
+ words: nil
59
61
  )
60
62
  )
61
63
  )
@@ -81,10 +83,11 @@ describe OllamaChat::Tools::GetURL do
81
83
  'ToolCall',
82
84
  function: double(
83
85
  name: 'get_url',
84
- arguments: double(
85
- url: 'https://www.example.com/foo',
86
- document_policy: nil,
87
- )
86
+ arguments: double(
87
+ url: 'https://www.example.com/foo',
88
+ document_policy: nil,
89
+ words: nil,
90
+ )
88
91
  )
89
92
  )
90
93
 
@@ -111,7 +114,7 @@ describe OllamaChat::Tools::GetURL do
111
114
  end
112
115
 
113
116
  it 'handles the "ignoring" policy' do
114
- args = double(url:, document_policy: 'ignoring')
117
+ args = double(url:, document_policy: 'ignoring', words: nil)
115
118
  tool_call = double(function: double(arguments: args))
116
119
 
117
120
  result = described_class.new.execute(tool_call, chat:)
@@ -120,7 +123,7 @@ describe OllamaChat::Tools::GetURL do
120
123
  end
121
124
 
122
125
  it 'handles the "importing" policy' do
123
- args = double(url:, document_policy: 'importing')
126
+ args = double(url:, document_policy: 'importing', words: nil)
124
127
  tool_call = double(function: double(arguments: args))
125
128
  expect(chat).to receive(:import_source).with(source_io, URI.parse(url)).and_return('imported content')
126
129
 
@@ -130,7 +133,7 @@ describe OllamaChat::Tools::GetURL do
130
133
  end
131
134
 
132
135
  it 'handles the "embedding" policy' do
133
- args = double(url:, document_policy: 'embedding')
136
+ args = double(url:, document_policy: 'embedding', words: nil)
134
137
  tool_call = double(function: double(arguments: args))
135
138
  expect(chat).to receive(:embed_source).with(source_io, URI.parse(url)).and_return('embedded content')
136
139
 
@@ -140,17 +143,29 @@ describe OllamaChat::Tools::GetURL do
140
143
  end
141
144
 
142
145
  it 'handles the "summarizing" policy' do
143
- args = double(url:, document_policy: 'summarizing')
146
+ args = double(url:, document_policy: 'summarizing', words: nil)
144
147
  tool_call = double(function: double(arguments: args))
145
- expect(chat).to receive(:summarize_source).with(source_io, URI.parse(url)).and_return('summarized content')
148
+ expect(chat).to receive(:summarize_source).with(source_io, URI.parse(url), words: 0).and_return('summarize prompt')
149
+ expect(chat).to receive(:generate).with(prompt: 'summarize prompt').and_return('summarized content')
146
150
 
147
151
  result = described_class.new.execute(tool_call, chat:)
148
152
  json = json_object(result)
149
153
  expect(json.content).to eq('summarized content')
150
154
  end
151
155
 
156
+ it 'handles the "summarizing" policy with custom words' do
157
+ args = double(url:, document_policy: 'summarizing', words: 50)
158
+ tool_call = double(function: double(arguments: args))
159
+ expect(chat).to receive(:summarize_source).with(source_io, URI.parse(url), words: 50).and_return('summarize prompt 50')
160
+ expect(chat).to receive(:generate).with(prompt: 'summarize prompt 50').and_return('short summary')
161
+
162
+ result = described_class.new.execute(tool_call, chat:)
163
+ json = json_object(result)
164
+ expect(json.content).to eq('short summary')
165
+ end
166
+
152
167
  it 'handles an invalid policy' do
153
- args = double(url:, document_policy: 'chaos_mode')
168
+ args = double(url:, document_policy: 'chaos_mode', words: nil)
154
169
  tool_call = double(function: double(arguments: args))
155
170
 
156
171
  result = described_class.new.execute(tool_call, chat:)
@@ -167,7 +182,7 @@ describe OllamaChat::Tools::GetURL do
167
182
  expect(chat).to receive(:fetch_source).and_yield(source_io)
168
183
  expect(chat).to receive(:add_image).with(chat.images, source_io, URI.parse(url))
169
184
 
170
- args = double(url:, document_policy: 'ignoring')
185
+ args = double(url:, document_policy: 'ignoring', words: nil)
171
186
  tool_call = double(function: double(arguments: args))
172
187
 
173
188
  result = described_class.new.execute(tool_call, chat:)
@@ -179,7 +194,7 @@ describe OllamaChat::Tools::GetURL do
179
194
  source_io = double('SourceIO', content_type: double(media_type: 'video'))
180
195
  expect(chat).to receive(:fetch_source).and_yield(source_io)
181
196
 
182
- args = double(url:, document_policy: 'ignoring')
197
+ args = double(url:, document_policy: 'ignoring', words: nil)
183
198
  tool_call = double(function: double(arguments: args))
184
199
 
185
200
  result = described_class.new.execute(tool_call, chat:)
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.117
4
+ version: 0.0.119
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
@@ -169,14 +169,14 @@ dependencies:
169
169
  requirements:
170
170
  - - ">="
171
171
  - !ruby/object:Gem::Version
172
- version: 0.6.0
172
+ version: 0.7.0
173
173
  type: :runtime
174
174
  prerelease: false
175
175
  version_requirements: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - ">="
178
178
  - !ruby/object:Gem::Version
179
- version: 0.6.0
179
+ version: 0.7.0
180
180
  - !ruby/object:Gem::Dependency
181
181
  name: unix_socks
182
182
  requirement: !ruby/object:Gem::Requirement