ollama_chat 0.0.124 → 0.0.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d2664dce7c6742ff5c950e64330b84325bcbb573467edde9aceddc3fa209641
4
- data.tar.gz: 87baa515acb398c9da1b7cf6c7fce173d946e4bd4d79ad0f1eb9dfbd2526235a
3
+ metadata.gz: 3de94c682281a4667e89feee589883bd33acf082165f37ce443072117a773167
4
+ data.tar.gz: 94cccbb92af48198a70879cf1028a0aebfe0cda49e25ccd82352cb35d9054bf7
5
5
  SHA512:
6
- metadata.gz: a091addc87c893f700f405147386dc8fe8fcc5c3134ad12bb1ffda570ec8a0f64ac4acbaeaed70116a6c61d0765e14e2f085fa8d2c0b98496fb0881a56fdb0f8
7
- data.tar.gz: 6d078a9c69b50719dda02e9c81a32d51929627ab6e50c7cae023630543e717dd2c1a86166495f339875178108e0ded60f9f451ebbec1275f16f06771c9b1b215
6
+ metadata.gz: d3bad30e34ad309fe6c2f2263802af2778eded9459affc6c6713c6383f58706fe54411e7a4b6f087c487718d0a6ea8e07e90dfed22ca1a64b563d207ce61287e
7
+ data.tar.gz: 8f9befaa4a6a519b8f7b4424f0d16380663b863e039f08142fc667b01ecb83aa989b9e635ea5373b22191c7ba8f568e97dd210d732d1d6d5099dd3e5cffb74ee
data/CHANGES.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-03 v0.0.125
4
+
5
+ ### Added
6
+
7
+ * Session-scoped trigger injection mechanism:
8
+ * New migration `011_add_trigger_to_sessions` adding a `:text` JSON column to
9
+ `sessions`.
10
+ * `Session#trigger` attribute with `plugin :serialization, :json` and `nil`
11
+ default.
12
+ * `CollectionSearch#trigger_inject` dispatch logic including embedding
13
+ guards, collection switching, document finding, and reranking.
14
+ * New `/session trigger edit` command registered in `commands.rb`.
15
+ * `edit_session_trigger` method in `session_management.rb` supporting dual
16
+ chooser and prefill.
17
+ * Trigger configuration display (⚡ block) added to `info_rag` in
18
+ `information.rb`.
19
+ * `runtime_information_inject` extracted from `chat.rb` into
20
+ `information.rb`.
21
+ * New prompt contexts: `prompts.rerank` (`default`, `memory_rerank`,
22
+ `lorebook_rerank`), `prompts.prompt.snippets_retrieval`, and
23
+ `prompts.prompt.snippets_trigger`.
24
+ * `persona_name` line added to `prompts.system.persona`.
25
+ * `TagResult` changed from `Data.define` to `Struct.new` to allow mutable
26
+ fields.
27
+ * `∅` fallback for empty patterns in `collection_stats`.
28
+ * Info-level logging for embedding requests/responses and rerank survivors in
29
+ `collection_search.rb`.
30
+ * New spec file `collection_search_spec.rb` with 7 examples for
31
+ `#trigger_inject`.
32
+
33
+ ### Changed
34
+
35
+ * `SessionLocking#lock?` now accepts a required `chat:` keyword argument to
36
+ decouple feedback delegation from the host object.
37
+ * `search_knowledge.rb` updated to use `snippets_retrieval` DB prompt,
38
+ `collection` block, and `database_collection?` guard.
39
+ * `@embedding` instance variable swept to `embedding` accessor in
40
+ `information.rb`, `source_fetching.rb`, and `web_searching.rb`.
41
+ * `Tins::Duration#format` used for compaction voice timing in
42
+ `report_compaction`, replacing `%.1f seconds`.
43
+ * `tins` dependency bumped from `~> **1.58**` to `~> **1.60**` to support
44
+ unpadded `%H`, `%M`, and `%u` directives.
45
+ * `resolve_tag_spec.rb` updated to replace an unreachable rescue-path test with
46
+ a "tagged file does not exist" test.
47
+ * `search_knowledge_spec.rb` updated for new prompt names, `collection` block,
48
+ and `tags_set` doubles.
49
+ * `compaction_spec.rb` regex updated to match the new duration format.
50
+ * `Tins::GO.go` flag string reordered in `bin/ollama_chat_log`.
51
+
52
+ ### Fixed
53
+
54
+ * Spec expectations in `session_management_spec.rb`, `parsing_spec.rb`, and
55
+ `source_fetching_spec.rb` updated to use Ruby 3.1+ hash shorthand `chat:`.
56
+
57
+ ### Tests
58
+
59
+ * Added specs for `CommandConcern` class methods:
60
+ * `help_message` via `OllamaChat::Commands`: table structure, category
61
+ headers, pattern filtering, subcommand expansion, and optional/mandatory
62
+ asterisks.
63
+ * `command_completions`: array shape, bare names, subcommand variants, and
64
+ sort order.
65
+ * `Command` class: `execute_if_match?` (match, captures, nil-regexp path,
66
+ no-context error), `completions` (product, optional bare, strict),
67
+ `command_names`, and `optional?`.
68
+ * Duplicate registration guard against the real `:copy` entry in
69
+ `OllamaChat::Commands`.
70
+ * Added specs for remaining command dispatch gaps:
71
+ * `/paste -i` stdin path via `paste_from_stdin`.
72
+ * `/last -v` voice output via `speak_last`.
73
+ * `/regenerate` success path (drop + return content).
74
+ * `/prompt delete` routes to `choose_and_delete_prompt`.
75
+ * `/web` without explicit count passes `nil`.
76
+ * `/character` JSON success paths: `info` (pager), `load` (raw content),
77
+ `import` (delegation).
78
+ * `/character` PNG `load` with `PNGMetadataExtractor`.
79
+ * Added specs for `SessionManagement#edit_session_trigger`:
80
+ * Setting trigger from `nil` to a configured hash with `enabled`,
81
+ `prompt_name`, and `text_count`.
82
+ * Updating config for an already-configured collection.
83
+ * Switching the trigger to a different collection name.
84
+ * Added specs for session model options and trigger edit:
85
+ * `/session model options` delegates to `edit_session_model_options` and
86
+ returns `:next`.
87
+ * `/session trigger edit` delegates to `edit_session_trigger` and returns
88
+ `:next`.
89
+
3
90
  ## 2026-10-02 v0.0.124
4
91
 
5
92
  ### Added
data/Rakefile CHANGED
@@ -49,7 +49,7 @@ GemHadar do
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
50
  dependency 'kramdown-ansi', '~> 0.5'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
- dependency 'tins', '~> 1.58'
52
+ dependency 'tins', '~> 1.60'
53
53
  dependency 'search_ui', '>= 0.2'
54
54
  dependency 'amatch', '~> 0.4'
55
55
  dependency 'pdf-reader', '~> 2.0'
data/bin/ollama_chat_log CHANGED
@@ -4,7 +4,7 @@ require 'ollama_chat'
4
4
  require 'file/tail'
5
5
  require 'tins/xt'
6
6
 
7
- opts = Tins::GO.go 'fm:n:h:', defaults: { ?n => 10 }
7
+ opts = Tins::GO.go 'hfm:n:', defaults: { ?n => 10 }
8
8
  log_path = ARGV.shift || OC::OLLAMA::CHAT::LOG::CHAT
9
9
 
10
10
  if opts[?h]
@@ -314,10 +314,11 @@ class OllamaChat::Chat
314
314
  # instance when a background thread was spawned (so callers can call
315
315
  # `cancel_speaking`), or `nil` for inline playback or when voice is off
316
316
  def speak(text, background: true)
317
+ session or return
317
318
  voice.on? or return
318
- voice = session.current_voice.full? or return
319
+ current_voice = session.current_voice.full? or return
319
320
  voice_handler.method_defined?(:speak) or return
320
- voice_handler.new(chat: self, voice:).speak(text, background:)
321
+ voice_handler.new(chat: self, voice: current_voice).speak(text, background:)
321
322
  end
322
323
 
323
324
  # Returns a human-readable string representation of the Chat object,
@@ -553,17 +554,8 @@ class OllamaChat::Chat
553
554
  group_uuid:
554
555
  )
555
556
 
556
- if runtime_info.on?
557
- tool_name = 'runtime_information'
558
- messages << OllamaChat::Message.new(
559
- role: 'user',
560
- tool_name: ,
561
- sender_name: tool_name,
562
- content: dynamic_runtime_information,
563
- images: @images.dup,
564
- group_uuid:
565
- )
566
- end
557
+ runtime_information_inject(group_uuid:)
558
+ trigger_inject(content, group_uuid:)
567
559
  end
568
560
  @images.clear
569
561
  handler = OllamaChat::FollowChat.new(
@@ -24,14 +24,37 @@ module OllamaChat::CollectionSearch
24
24
  tags = Documentrix::Utils::Tags.new(
25
25
  tags, valid_tag: /\A#*([-\w.\]\[]+)/
26
26
  )
27
- @documents.find_where(
28
- query.first(config.embedding.model.context_length),
27
+ effective_query = query.first(
28
+ config.embedding.model.context_length
29
+ )
30
+ embedding_prompt = config.embedding.model.prompt?
31
+ log(
32
+ :info, 'Embedding search request',
33
+ data: {
34
+ model: config.embedding.model.to_h,
35
+ collection: collection,
36
+ query: effective_query,
37
+ prompt: embedding_prompt,
38
+ }
39
+ )
40
+ records = @documents.find_where(
41
+ effective_query,
29
42
  tags:,
30
- prompt: config.embedding.model.prompt?,
43
+ prompt: embedding_prompt,
31
44
  text_size:,
32
45
  text_count:,
33
46
  min_similarity:
34
47
  )
48
+ log(
49
+ :info, 'Embedding search response',
50
+ data: {
51
+ collection: collection,
52
+ query: effective_query,
53
+ result_count: records.size,
54
+ top: records.first&.similarity&.round(4),
55
+ }
56
+ )
57
+ records
35
58
  end
36
59
 
37
60
  # Reranks a set of records using the LLM with the `rerank` prompt.
@@ -43,11 +66,11 @@ module OllamaChat::CollectionSearch
43
66
  # @return [Array<Documentrix::Utils::TagResult>] filtered records
44
67
  def rerank_records(query, records, prompt_name: nil)
45
68
  candidates = records.each_with_index.map { |r, i|
46
- "[#{i}] #{truncate(r.text.strip, length: 300)}"
69
+ "[#{i}] #{r.tags_set.to_s(link: false)} #{truncate(r.text.squeeze, length: 300)}"
47
70
  }.join("\n")
48
71
 
49
- prompt_name ||= 'rerank'
50
- rerank_prompt = prompt(prompt_name) or
72
+ prompt_name ||= 'default'
73
+ rerank_prompt = prompt(prompt_name, context: 'rerank') or
51
74
  raise 'missing prompt %s' % prompt_name.inspect
52
75
  rerank_prompt = rerank_prompt.to_s
53
76
  .named_placeholders_interpolate({ query:, candidates: })
@@ -63,4 +86,77 @@ module OllamaChat::CollectionSearch
63
86
  end
64
87
  records
65
88
  end
89
+
90
+ # Injects relevant snippets from the session's trigger collection
91
+ # into the conversation as a tool message.
92
+ #
93
+ # Reads the first entry of the session's `trigger` hash, performs a
94
+ # vector search in the configured collection, reranks the candidates
95
+ # via the LLM, and appends a `trigger_inject` tool message containing
96
+ # the surviving snippets (text, similarity, tags). The message is
97
+ # grouped with the current user message via `group_uuid`.
98
+ #
99
+ # @param content [String] the parsed user message text used as the
100
+ # search query
101
+ # @param group_uuid [String, nil] the group UUID for message grouping
102
+ # (shared with the user message and runtime-info message)
103
+ #
104
+ # @return [self, nil] self if snippets were injected, nil if no
105
+ # trigger is configured, the trigger or embedding is disabled, the
106
+ # collection or prompt is missing, or no snippets survived reranking
107
+ def trigger_inject(content, group_uuid:)
108
+ embedding.on? or return
109
+ trigger = session.trigger&.first or return
110
+ collection, config = trigger
111
+ unless config['enabled']
112
+ log(:info, 'Trigger for collection %s disabled.' % collection)
113
+ return
114
+ end
115
+ unless col = database_collection?(collection)
116
+ log(:error, 'Unknown collection named %s' % collection)
117
+ return
118
+ end
119
+ prompt_name = config['prompt_name']
120
+ unless rerank_prompt = prompt(prompt_name, context: 'rerank').full?(:to_s)
121
+ log(
122
+ :error, 'Unknown rerank prompt named %s' % prompt_name,
123
+ data: { config: }
124
+ )
125
+ return
126
+ end
127
+ records = []
128
+ switch_collection(collection) do
129
+ records = find_document_records(content, text_count: config['text_count'])
130
+ records.empty? and return
131
+ pre_rerank = records.size
132
+ records = rerank_records(content, records, prompt_name:)
133
+ log(:info, 'Trigger: %d/%d passed rerank for %s' % [ records.size, pre_rerank, collection ],
134
+ data: { collection:, prompt_name: })
135
+ records.empty? and return
136
+ end
137
+ snippets = records.map { |record|
138
+ {
139
+ text: record.text,
140
+ similarity: record.similarity.to_f,
141
+ tags: record.tags_set.to_s(link: false),
142
+ }
143
+ }
144
+ message_content = {
145
+ prompt: prompt('snippets_trigger').to_s,
146
+ collection: {
147
+ name: collection,
148
+ description: col.description&.to_s,
149
+ },
150
+ snippets:
151
+ }.to_json
152
+ tool_name = 'trigger_inject'
153
+ messages << OllamaChat::Message.new(
154
+ role: 'user',
155
+ tool_name: ,
156
+ sender_name: tool_name,
157
+ content: message_content,
158
+ group_uuid:
159
+ )
160
+ self
161
+ end
66
162
  end
@@ -113,15 +113,16 @@ module OllamaChat::Commands
113
113
 
114
114
  command(
115
115
  name: :session,
116
- regexp: %r(^/session(?:\s+(change|previous|list|new|duplicate|rename|delete|model options change|model options))?((?:\s+-(?:p\s*\w+))*)(?:\s+([^-].*))?$),
117
- complete: [ 'session', %w[ change previous list new duplicate rename delete model\ options\ change model\ options ] ],
116
+ regexp: %r(^/session(?:\s+(change|previous|list|new|duplicate|rename|delete|model options change|model options|trigger edit))?((?:\s+-(?:p\s*\w+))*)(?:\s+([^-].*))?$),
117
+ complete: [ 'session', %w[ change previous list new duplicate rename delete model\ options\ change model\ options trigger\ edit ] ],
118
118
  optional: true,
119
119
  options: "[-p profile]\n[name]",
120
120
  help: <<~EOT
121
121
  💬 Manage sessions:
122
122
  - list/new/delete/rename/duplicate
123
123
  - change [name]/previous
124
- - model options/change
124
+ - model options/change
125
+ - trigger edit
125
126
  EOT
126
127
  ) do |subcommand, opts, name|
127
128
  case subcommand
@@ -152,6 +153,8 @@ module OllamaChat::Commands
152
153
  else
153
154
  feedback("No previous session defined.", type: :info)
154
155
  end
156
+ when 'trigger edit'
157
+ edit_session_trigger
155
158
  end
156
159
  :next
157
160
  end
@@ -295,11 +295,15 @@ module OllamaChat::Compaction
295
295
  Context: #{result.context_before} → #{result.context_after}
296
296
  Stored total: #{result.stored_total}
297
297
  EOT
298
+ duration = Tins::Duration.new(duration)
299
+ duration_string = duration.format(
300
+ duration.minutes? ? '%M minutes and %u seconds' : '%u seconds'
301
+ )
298
302
  speak(
299
- 'Compressed conversation context from %.f to %.f in %.1f seconds.' % [
303
+ 'Compressed conversation context from %.f to %.f in %s.' % [
300
304
  100 * result.context_filled_before,
301
305
  100 * result.context_filled_after,
302
- duration,
306
+ duration_string,
303
307
  ]
304
308
  )
305
309
  end
@@ -0,0 +1,7 @@
1
+ Sequel.migration do
2
+ change do
3
+ alter_table :sessions do
4
+ add_column :trigger, :text
5
+ end
6
+ end
7
+ end
@@ -12,6 +12,7 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
12
12
  plugin :touch
13
13
  plugin :serialization, :json, :tools_default_enabled
14
14
  plugin :serialization, :json, :model_options
15
+ plugin :serialization, :json, :trigger
15
16
  plugin :validation_helpers
16
17
 
17
18
  # Validates the session instance.
@@ -133,6 +134,15 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
133
134
  # @!attribute [v] model_options
134
135
  # @return [String, nil] A JSON-serialized string containing model-specific options.
135
136
  #
137
+ # @!attribute [v] trigger
138
+ # @return [Hash, nil] Maps collection names to search/rerank
139
+ # overrides, e.g.
140
+ # `{ 'memory-miyu' => { 'prompt_name' => 'memory_rerank',
141
+ # 'text_count' => 5 } }`.
142
+ # Recognized keys: `prompt_name`, `text_count`, `text_size`,
143
+ # `min_similarity`.
144
+ # `nil` when the trigger mechanism is disabled for this session.
145
+ #
136
146
  # @!attribute [v] messages
137
147
  # @return [String] The full conversation history, stored in JSONL format.
138
148
  #
@@ -184,6 +194,7 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
184
194
  context_format: chat.config.context.format,
185
195
  working_directory: Dir.pwd,
186
196
  messages: '',
197
+ trigger: nil,
187
198
  }
188
199
  new(attributes)
189
200
  end
@@ -25,10 +25,11 @@ module OllamaChat::Database::SessionLocking
25
25
 
26
26
  # Attempts to acquire a lock on the session.
27
27
  #
28
+ # @param chat [OllamaChat] the chat instance that receives feedback
28
29
  # @return [Boolean] true if the lock was successfully acquired, false if already locked.
29
- def lock?
30
+ def lock?(chat:)
30
31
  if running_pid = locked?
31
- feedback("session #{id} locked by running process #{running_pid}", type: :warn)
32
+ chat.feedback("session #{id} locked by running process #{running_pid}", type: :warn)
32
33
  false
33
34
  else
34
35
  lock
@@ -81,7 +81,7 @@ module OllamaChat::Information
81
81
  Current Collection
82
82
  Name: #{bold{collection}}
83
83
  Status: #{col&.enabled ? '✅' : '⛔'}
84
- Patterns: #{italic{col&.patterns&.join(' ')}}
84
+ Patterns: #{italic{col&.patterns.full?(:join, ' ') || '∅' }}
85
85
  #Embeddings: #{@documents.size}
86
86
  #Tags: #{@documents.tags.size}
87
87
  Tags:
@@ -190,15 +190,23 @@ module OllamaChat::Information
190
190
  #
191
191
  # @param output [IO] the output stream to write the information to, defaults to STDOUT
192
192
  def info_rag(output: STDOUT)
193
- if @embedding.on?
193
+ if embedding.on?
194
194
  output.puts "🗄️ Current RAG model is #{bold{@embedding_model}}"
195
195
  if @embedding_model_options.present?
196
196
  output.puts " Options: #{JSON.pretty_generate(@embedding_model_options).gsub(/(?<!\A)^/, ' ')}"
197
197
  end
198
198
  output.puts "Text splitter is #{bold{config.embedding.splitter.name}}."
199
199
  collection_stats(output:)
200
+ if session.trigger.full?
201
+ session.trigger.each do |col, cfg|
202
+ state = cfg['enabled'] ? '✅' : '⛔'
203
+ output.puts "\u26A1 Trigger: #{state} #{bold{col}}"
204
+ output.puts " Prompt: #{bold{cfg['prompt_name']}}"
205
+ output.puts " Text count: #{cfg['text_count'] || 'unlimited'}"
206
+ end
207
+ end
200
208
  end
201
- @embedding.show(output:)
209
+ embedding.show(output:)
202
210
  output.puts "📜 Document policy for parsing in user text: #{bold{document_policy}}"
203
211
  end
204
212
 
@@ -418,6 +426,30 @@ module OllamaChat::Information
418
426
  prompt(:dynamic_runtime_info).to_s % dynamic_runtime_information_values
419
427
  end
420
428
 
429
+ # Injects the current dynamic runtime information (the "heartbeat")
430
+ # as a user-role tool message into the active message list.
431
+ #
432
+ # The injected message shares the caller's +group_uuid+ so it is
433
+ # grouped with the surrounding conversation exchange. The content
434
+ # is generated by +dynamic_runtime_information+, which interpolates
435
+ # live values (timestamp, git state, terminal size, context usage)
436
+ # into the configured prompt template.
437
+ #
438
+ # No-op when the +runtime_info+ switch is off.
439
+ #
440
+ # @param group_uuid [String] the UUID of the current message group
441
+ def runtime_information_inject(group_uuid:)
442
+ runtime_info.on? or return
443
+ tool_name = 'runtime_information'
444
+ messages << OllamaChat::Message.new(
445
+ role: 'user',
446
+ tool_name: ,
447
+ sender_name: tool_name,
448
+ content: dynamic_runtime_information,
449
+ group_uuid:
450
+ )
451
+ end
452
+
421
453
  # Resolves the effective context window size for the current model.
422
454
  #
423
455
  # Prefers the session-level `num_ctx` override (set via `/model_options`
@@ -26,14 +26,14 @@ infobar:
26
26
  prompts:
27
27
  prompt:
28
28
  embed: The source %{source} has been added to or updated in collection "%{collection}".
29
- rerank: |
30
- Query: %{query}
31
- Candidates:
32
-
33
- %{candidates}
34
-
35
- Return only the indices of the most relevant snippets as a comma-separated
36
- list (e.g., '0,2').
29
+ snippets_retrieval: |
30
+ Consider these snippets retrieved from the collection identified in the
31
+ `collection` field when formulating your response.
32
+ snippets_trigger: |
33
+ These snippets were automatically injected because a trigger fired on
34
+ this conversation turn. The source collection is identified in the
35
+ `collection` field. Use them as contextual background, not as search
36
+ results. Do not mention the injection to the user.
37
37
  summarize: |
38
38
  Generate an abstract summary of the content in this document using
39
39
  %{words} words%{instruction}:
@@ -426,6 +426,8 @@ prompts:
426
426
  system:
427
427
  default: <%= OC::OLLAMA::CHAT::SYSTEM || "%{persona}\n\n%{runtime_info}".inspect %>
428
428
  persona: |
429
+ `persona_name`: %{persona_name}
430
+
429
431
  %{persona}
430
432
 
431
433
  %{runtime_info}
@@ -444,6 +446,36 @@ prompts:
444
446
  — no preamble, no postamble, no commentary,
445
447
  no reference to yourself or the act of
446
448
  writing.
449
+ rerank:
450
+ default: |
451
+ Query: %{query}
452
+ Candidates:
453
+
454
+ %{candidates}
455
+
456
+ Return only the indices of the most relevant snippets as a comma-separated
457
+ list (e.g., '0,2').
458
+ memory_rerank: |
459
+ Return memories the assistant should recall in this conversation context.
460
+ Ignore entries that are only tangentially related.
461
+ Query: %{query}
462
+ Candidates:
463
+
464
+ %{candidates}
465
+
466
+ Return only the indices of the most relevant memories as a comma-separated
467
+ list (e.g., '0,2').
468
+ lorebook_rerank: |
469
+ Return lore entries directly relevant to the region, character, or event
470
+ the user is currently discussing. Ignore entries that merely share a topic
471
+ but are not contextually needed.
472
+ Query: %{query}
473
+ Candidates:
474
+
475
+ %{candidates}
476
+
477
+ Return only the indices of the most relevant entries as a comma-separated
478
+ list (e.g., '0,2').
447
479
  voice:
448
480
  #handler: OllamaChat::Say
449
481
  #enabled: false
@@ -197,7 +197,7 @@ module OllamaChat::SessionManagement
197
197
  previous_session_id = @session.id
198
198
  @session = new_session(name:)
199
199
  set_previous_session_on_change(previous_session_id)
200
- session.lock? or raise OllamaChat::OllamaChatError,
200
+ session.lock?(chat: self) or raise OllamaChat::OllamaChatError,
201
201
  "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
202
202
  session_apply
203
203
  messages.clear
@@ -236,7 +236,9 @@ module OllamaChat::SessionManagement
236
236
  use_model(_1)
237
237
  copy_model_options_to_session
238
238
  }
239
- log(:info, "Session duplicated", data: { session_id: session.id, name: session.name, old_session_id: old_session.id })
239
+ log(:info, "Session duplicated", data: {
240
+ session_id: session.id, name: session.name, old_session_id: old_session.id
241
+ })
240
242
  nil
241
243
  end
242
244
 
@@ -253,7 +255,7 @@ module OllamaChat::SessionManagement
253
255
  preferred_session
254
256
  end
255
257
  session or abort "No session named #{bold{session_name.inspect}} found."
256
- if session.lock?
258
+ if session.lock?(chat: self)
257
259
  messages.read_conversation_jsonl(session.messages.to_s)
258
260
  session_apply
259
261
  else
@@ -483,6 +485,80 @@ module OllamaChat::SessionManagement
483
485
  end
484
486
  end
485
487
 
488
+ # Interactively edits the trigger configuration for a collection.
489
+ #
490
+ # Presents a chooser for available rerank prompt names, then prompts
491
+ # for the collection, text_count, and enabled state. Existing values
492
+ # are prefilled when re-editing a known trigger so the user can simply
493
+ # accept defaults or toggle enabled off.
494
+ def edit_session_trigger
495
+ prompts = models::Prompt.where(context: 'rerank').map(&:name).sort
496
+ return feedback('No rerank prompts available.', type: :warn) \
497
+ unless prompts.any?
498
+
499
+ collections = all_collections.map(&:name).sort
500
+ return feedback('No collections available.', type: :warn) \
501
+ unless collections.any?
502
+
503
+ existing = session.trigger.to_h
504
+
505
+ collection = nil
506
+ choose_with_state do
507
+ # Prefill with the first configured collection as a starting
508
+ # point. Fine for the single-trigger case; with multiple
509
+ # configured, it's just an arbitrary default the user can
510
+ # override in the chooser.
511
+ if selected = existing.first&.first&.dup
512
+ self.current_search_state = SearchUI::Search::State.new(selected, 0)
513
+ end
514
+ collection = choose_entry(collections, prompt: 'Collection? %s')
515
+ return feedback('Exiting chooser.', type: :info) unless collection
516
+ end
517
+
518
+ prompt_name = nil
519
+ choose_with_state do
520
+ if selected = existing.dig(collection, 'prompt_name')
521
+ self.current_search_state = SearchUI::Search::State.new(selected, 0)
522
+ end
523
+ prompt_name = choose_entry(prompts, prompt: 'Rerank prompt? %s')
524
+ return feedback('Exiting chooser.', type: :info) unless prompt_name
525
+ end
526
+
527
+ prefill_tc = ''
528
+ prefill_enabled = 'y'
529
+ if cfg = existing[collection]
530
+ prefill_tc = cfg['text_count'].to_s
531
+ prefill_enabled = cfg['enabled'] ? 'y' : 'n'
532
+ end
533
+
534
+ tc_raw = ask?(
535
+ prompt: 'text_count (empty for no limit): ',
536
+ prefill: prefill_tc
537
+ )
538
+ return feedback('Cancelled.', type: :cancel) if tc_raw.nil?
539
+ text_count = tc_raw.strip.empty? ? nil : tc_raw.strip.to_i
540
+
541
+ enabled_raw = ask?(
542
+ prompt: 'Enabled? (y/n): ',
543
+ prefill: prefill_enabled
544
+ )
545
+ return feedback('Cancelled.', type: :cancel) if enabled_raw.nil?
546
+ enabled = enabled_raw.strip.match?(/\Ay/i)
547
+
548
+ cfg = {
549
+ 'enabled' => enabled,
550
+ 'prompt_name' => prompt_name,
551
+ }
552
+ cfg['text_count'] = text_count if text_count
553
+
554
+ session.update(trigger: { collection => cfg })
555
+
556
+ feedback(
557
+ "Trigger for #{collection.inspect} → #{cfg.inspect}.",
558
+ type: :info
559
+ )
560
+ end
561
+
486
562
  # Derives a title for the session based on its content.
487
563
  #
488
564
  # @param length [Integer] the maximum length of the title (default: 128)
@@ -551,7 +627,7 @@ module OllamaChat::SessionManagement
551
627
  session_close
552
628
  previous_session = session
553
629
  @session = chosen_session
554
- if session.lock?
630
+ if session.lock?(chat: self)
555
631
  messages.read_conversation_jsonl(session.messages.to_s)
556
632
  if current_collection = session.current_collection.full? and
557
633
  database_collection?(current_collection)
@@ -194,7 +194,7 @@ module OllamaChat::SourceFetching
194
194
  # @return [Array, String, nil] The embedded chunks or processed content, or
195
195
  # nil if embedding is disabled or fails
196
196
  def embed_source(source_io, source, tags: [], count: nil)
197
- @embedding.on? or return parse_source(source_io)
197
+ embedding.on? or return parse_source(source_io)
198
198
  unless @documents.source_modified?(source)
199
199
  infobar.puts "Source #{source.to_s.inspect} already up-to-date. => Skipping."
200
200
  log(:info, "Source up-to-date", data: { source: source.to_s })
@@ -272,7 +272,7 @@ module OllamaChat::SourceFetching
272
272
  # nil if the operation fails
273
273
  def embed(source, tags: [], prompt: prompt(:embed))
274
274
  prompt = prompt.to_s
275
- @embedding.on? or return
275
+ embedding.on? or return
276
276
  fetch_source(source) do |source_io|
277
277
  content = parse_source(source_io)
278
278
  content.present? or return