ollama_chat 0.0.113 → 0.0.115
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 +4 -4
- data/CHANGES.md +120 -0
- data/README.md +7 -0
- data/bin/ollama_chat_log +1 -1
- data/bin/ollama_chat_send +3 -1
- data/lib/ollama_chat/chat.rb +30 -14
- data/lib/ollama_chat/commands.rb +36 -18
- data/lib/ollama_chat/compaction.rb +289 -0
- data/lib/ollama_chat/config_handling.rb +32 -0
- data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
- data/lib/ollama_chat/database/models/app_state.rb +48 -16
- data/lib/ollama_chat/database/models/session.rb +1 -2
- data/lib/ollama_chat/database.rb +4 -2
- data/lib/ollama_chat/information.rb +53 -6
- data/lib/ollama_chat/logging.rb +3 -2
- data/lib/ollama_chat/message.rb +22 -1
- data/lib/ollama_chat/message_format.rb +1 -2
- data/lib/ollama_chat/message_list.rb +223 -9
- data/lib/ollama_chat/model_handling.rb +1 -1
- data/lib/ollama_chat/oc.rb +17 -7
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +72 -3
- data/lib/ollama_chat/parsing.rb +2 -1
- data/lib/ollama_chat/prompt_handling.rb +1 -1
- data/lib/ollama_chat/prompt_management.rb +1 -0
- data/lib/ollama_chat/rag_handling.rb +19 -5
- data/lib/ollama_chat/session_management.rb +5 -3
- data/lib/ollama_chat/token_estimator/crude.rb +1 -1
- data/lib/ollama_chat/token_estimator.rb +1 -1
- data/lib/ollama_chat/tools/concern.rb +17 -0
- data/lib/ollama_chat/tools/directory_structure.rb +11 -7
- data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
- data/lib/ollama_chat/tools/execute_shell.rb +158 -0
- data/lib/ollama_chat/tools/get_rfc.rb +2 -0
- data/lib/ollama_chat/tools/lookup_group.rb +100 -0
- data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
- data/lib/ollama_chat/tools/patch_file.rb +23 -5
- data/lib/ollama_chat/tools/read_file.rb +1 -1
- data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
- data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
- data/lib/ollama_chat/tools/write_file.rb +25 -6
- data/lib/ollama_chat/tools.rb +2 -0
- data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
- data/lib/ollama_chat/utils/fetcher.rb +1 -1
- data/lib/ollama_chat/utils/log_rotation.rb +29 -0
- data/lib/ollama_chat/utils/strip_ansi.rb +22 -0
- data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
- data/lib/ollama_chat/utils.rb +2 -0
- data/lib/ollama_chat/version.rb +1 -1
- data/lib/ollama_chat.rb +7 -0
- data/ollama_chat.gemspec +5 -5
- data/spec/ollama_chat/commands_spec.rb +25 -0
- data/spec/ollama_chat/compaction_spec.rb +381 -0
- data/spec/ollama_chat/config_handling_spec.rb +60 -0
- data/spec/ollama_chat/message_list_spec.rb +229 -0
- data/spec/ollama_chat/message_spec.rb +143 -0
- data/spec/ollama_chat/rag_handling_spec.rb +3 -0
- data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
- data/spec/ollama_chat/tools/browse_spec.rb +7 -0
- data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
- data/spec/ollama_chat/tools/concern_spec.rb +77 -0
- data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
- data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
- data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
- data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
- data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
- data/spec/ollama_chat/tools/execute_shell_spec.rb +232 -0
- data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
- data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
- data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
- data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
- data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
- data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
- data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
- data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
- data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
- data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
- data/spec/ollama_chat/tools/read_file_spec.rb +10 -3
- data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
- data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
- data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
- data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
- data/spec/ollama_chat/tools/write_file_spec.rb +98 -2
- data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
- data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
- data/spec/spec_helper.rb +2 -1
- metadata +25 -1
|
@@ -70,6 +70,212 @@ class OllamaChat::MessageList
|
|
|
70
70
|
@messages.size
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Finds the message index at which compaction should cut.
|
|
74
|
+
#
|
|
75
|
+
# Walks non-system message groups (from +start+ onward) backward from
|
|
76
|
+
# the tail, accumulating per-group token estimates. Returns the index of
|
|
77
|
+
# the first message in the group whose addition causes the running total
|
|
78
|
+
# to meet or exceed +keep_recent_tokens+. Messages from +start+ up to
|
|
79
|
+
# that index are candidates for summarization; messages from that index
|
|
80
|
+
# onward are preserved.
|
|
81
|
+
#
|
|
82
|
+
# If every group together still fits under the budget, the index of the
|
|
83
|
+
# first group at or after +start+ is returned (signalling a no-op cut).
|
|
84
|
+
#
|
|
85
|
+
# @param start [Integer] the index at which to begin scanning (messages
|
|
86
|
+
# before this index are excluded; typically the position just past an
|
|
87
|
+
# existing summary message).
|
|
88
|
+
# @param keep_recent_tokens [Integer] the token budget for preserved
|
|
89
|
+
# (recent) messages.
|
|
90
|
+
# @return [Integer, nil] the cut index into +@messages+, or +nil+ if
|
|
91
|
+
# there are no non-system groups at or after +start+.
|
|
92
|
+
def find_cut_point(start:, keep_recent_tokens:)
|
|
93
|
+
groups = []
|
|
94
|
+
@messages.each_with_index do |msg, i|
|
|
95
|
+
next if msg.role == 'system'
|
|
96
|
+
next if i < start
|
|
97
|
+
tokens = msg.token_estimate(
|
|
98
|
+
strip_thinking: @chat.think_strip.on?
|
|
99
|
+
).tokens
|
|
100
|
+
uuid = msg.group_uuid
|
|
101
|
+
if last = groups.last and last[:uuid] == uuid
|
|
102
|
+
last[:tokens] += tokens
|
|
103
|
+
else
|
|
104
|
+
groups << { uuid:, start: i, tokens: }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
return nil if groups.empty?
|
|
108
|
+
|
|
109
|
+
accumulated = 0
|
|
110
|
+
groups.reverse_each do |group|
|
|
111
|
+
accumulated += group[:tokens]
|
|
112
|
+
return group[:start] if accumulated >= keep_recent_tokens
|
|
113
|
+
end
|
|
114
|
+
groups.first[:start]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Finds the most recent summary message in the list.
|
|
118
|
+
#
|
|
119
|
+
# Scans backward from the tail for a message with
|
|
120
|
+
# `role == 'tool'` and `tool_name == 'summary'`.
|
|
121
|
+
#
|
|
122
|
+
# @return [OllamaChat::Message, nil] the summary message, or
|
|
123
|
+
# `nil` if no summary has been inserted yet.
|
|
124
|
+
def find_summary
|
|
125
|
+
messages.reverse_each.find do |m|
|
|
126
|
+
m.role == 'tool' && m.tool_name == 'summary'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Compacts the message list by summarizing old groups and inserting
|
|
131
|
+
# a single summary tool message at the cut point.
|
|
132
|
+
#
|
|
133
|
+
# Resolves the keep-recent token budget from the current model's context
|
|
134
|
+
# length, finds the cut point, summarizes all groups before it (passing
|
|
135
|
+
# any existing summary as +previous_summary+ for iterative re-compaction),
|
|
136
|
+
# then inserts the new summary at the cut point. Old groups remain in
|
|
137
|
+
# the list for retrieval via +lookup_group+; the LLM context builder
|
|
138
|
+
# skips them on the next call.
|
|
139
|
+
#
|
|
140
|
+
# @return [OllamaChat::Compaction::Result, nil] a +Result+ with
|
|
141
|
+
# before/after context estimates on success, or +nil+ if there was
|
|
142
|
+
# nothing to compact.
|
|
143
|
+
def compact!
|
|
144
|
+
ctx = @chat.current_context_length
|
|
145
|
+
budget = @chat.compact_ratio_tokens(:keep_recent, ctx)
|
|
146
|
+
|
|
147
|
+
existing_summary = find_summary
|
|
148
|
+
start = @messages.each_with_index.find do |m, i|
|
|
149
|
+
m.role == 'tool' && m.tool_name == 'summary' and break i + 1
|
|
150
|
+
end
|
|
151
|
+
start ||= 0
|
|
152
|
+
start = start.clamp(0..(@messages.size - 1))
|
|
153
|
+
|
|
154
|
+
# Cut among post-summary groups; the old summary is excluded
|
|
155
|
+
# from the budget window by +start+.
|
|
156
|
+
cut = find_cut_point(start:, keep_recent_tokens: budget)
|
|
157
|
+
return nil if cut.nil? || cut <= 1
|
|
158
|
+
|
|
159
|
+
candidates = @messages[start...cut]
|
|
160
|
+
.reject { |m| m.role == 'system' }
|
|
161
|
+
.reject { |m| m.role == 'tool' && m.tool_name == 'summary' }
|
|
162
|
+
return nil if candidates.empty?
|
|
163
|
+
|
|
164
|
+
cand_es = OllamaChat::TokenEstimator::Crude.new(
|
|
165
|
+
candidates.sum { |m| m.content.to_s.bytesize }
|
|
166
|
+
).perform
|
|
167
|
+
context_before = @chat.context_usage
|
|
168
|
+
|
|
169
|
+
@chat.log(:info, 'Compaction: starting', data: {
|
|
170
|
+
context_length: ctx,
|
|
171
|
+
budget: budget,
|
|
172
|
+
cut_index: cut,
|
|
173
|
+
candidates: candidates.size,
|
|
174
|
+
candidate_bytes: cand_es.bytes_formatted,
|
|
175
|
+
candidate_tokens: cand_es.tokens_formatted,
|
|
176
|
+
previous_summary: existing_summary ? 'yes' : 'no',
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
summary_text, all_tool_entries = @chat.summarize_for_compaction(
|
|
180
|
+
messages: candidates,
|
|
181
|
+
previous_summary: existing_summary,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
compact_es = OllamaChat::TokenEstimator.estimate(summary_text.bytesize)
|
|
185
|
+
@chat.log(:info, 'Compaction: summary generated', data: {
|
|
186
|
+
summary_bytes: compact_es.bytes_formatted,
|
|
187
|
+
summary_tokens: compact_es.tokens_formatted,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
summary_msg = OllamaChat::Message.new(
|
|
191
|
+
role: 'tool',
|
|
192
|
+
tool_name: 'summary',
|
|
193
|
+
content: summary_text,
|
|
194
|
+
tool_calls: all_tool_entries,
|
|
195
|
+
).initialize_group_uuid
|
|
196
|
+
|
|
197
|
+
# Remove old summary (if any) and adjust cut for the shift.
|
|
198
|
+
if existing_summary
|
|
199
|
+
idx = @messages.index(existing_summary)
|
|
200
|
+
@messages.delete_at(idx)
|
|
201
|
+
cut -= 1 if idx < cut
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
@messages.insert(cut, summary_msg)
|
|
205
|
+
@chat.log(:info, 'Compaction: done', data: {
|
|
206
|
+
total_messages: @messages.size,
|
|
207
|
+
summary_at: cut,
|
|
208
|
+
})
|
|
209
|
+
sync
|
|
210
|
+
|
|
211
|
+
OllamaChat::Compaction::Result.new(
|
|
212
|
+
context_before:,
|
|
213
|
+
context_after: @chat.context_usage,
|
|
214
|
+
candidates: candidates.size,
|
|
215
|
+
candidate_size: "#{cand_es.bytes_formatted} / " \
|
|
216
|
+
"#{cand_es.tokens_formatted}",
|
|
217
|
+
summary_size: "#{compact_es.bytes_formatted} / " \
|
|
218
|
+
"#{compact_es.tokens_formatted}",
|
|
219
|
+
stored_total: @chat.conversation_length,
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the messages that will actually be sent to the LLM.
|
|
224
|
+
#
|
|
225
|
+
# If no summary message exists, returns all messages (identical to
|
|
226
|
+
# +to_ary+). If a summary is present, returns only the system prompt,
|
|
227
|
+
# the summary, and everything after it — groups before the summary
|
|
228
|
+
# are invisible to the model but remain in the list for
|
|
229
|
+
# +lookup_group+ retrieval.
|
|
230
|
+
#
|
|
231
|
+
# @return [Array<OllamaChat::Message>] the messages to send to the LLM
|
|
232
|
+
def compacted_messages
|
|
233
|
+
idx = @messages.rindex do |m|
|
|
234
|
+
m.role == 'tool' && m.tool_name == 'summary'
|
|
235
|
+
end
|
|
236
|
+
return to_ary if idx.nil?
|
|
237
|
+
|
|
238
|
+
system = @messages.take_while { |m| m.role == 'system' }
|
|
239
|
+
system + [@messages[idx]] + @messages[(idx + 1)..]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Estimates the token and byte size of the messages that will actually
|
|
243
|
+
# be sent to the LLM (i.e. +compacted_messages+).
|
|
244
|
+
#
|
|
245
|
+
# When no summary message exists this is identical to the full list.
|
|
246
|
+
# When a summary is present, the pre-summary groups are excluded.
|
|
247
|
+
#
|
|
248
|
+
# Uses per-message +token_estimate+ which counts only text content
|
|
249
|
+
# (and thinking, unless +think_strip+ is enabled), excluding images
|
|
250
|
+
# and metadata scaffolding that would inflate a raw serialization.
|
|
251
|
+
#
|
|
252
|
+
# @return [OllamaChat::TokenEstimator::Estimate] the estimated token and
|
|
253
|
+
# byte counts for the effective LLM payload.
|
|
254
|
+
def compacted_estimate_tokens
|
|
255
|
+
strip = @chat.think_strip.on?
|
|
256
|
+
bytes = compacted_messages.sum {
|
|
257
|
+
_1.token_estimate(strip_thinking: strip).bytes
|
|
258
|
+
}
|
|
259
|
+
OllamaChat::TokenEstimator.estimate(bytes)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Estimates the token and byte size of the **full** stored message
|
|
263
|
+
# list (i.e. +@messages+), using per-message +token_estimate+.
|
|
264
|
+
#
|
|
265
|
+
# Unlike +Session#estimate_tokens+ which measures raw JSONL bytes
|
|
266
|
+
# (including base64 images and JSON scaffolding), this counts only
|
|
267
|
+
# text content and thinking (unless +think_strip+ is enabled).
|
|
268
|
+
#
|
|
269
|
+
# @return [OllamaChat::TokenEstimator::Estimate] the estimated token
|
|
270
|
+
# and byte counts for the full conversation payload.
|
|
271
|
+
def full_estimate_tokens
|
|
272
|
+
strip = @chat.think_strip.on?
|
|
273
|
+
bytes = @messages.sum {
|
|
274
|
+
_1.token_estimate(strip_thinking: strip).bytes
|
|
275
|
+
}
|
|
276
|
+
OllamaChat::TokenEstimator.estimate(bytes)
|
|
277
|
+
end
|
|
278
|
+
|
|
73
279
|
# The clear method removes all non-system messages from the message list.
|
|
74
280
|
#
|
|
75
281
|
# @return [ OllamaChat::MessageList ] self
|
|
@@ -148,6 +354,8 @@ class OllamaChat::MessageList
|
|
|
148
354
|
def each_message(role: %w[ user assistant ], tool: false, &block)
|
|
149
355
|
block or return enum_for(__method__, role:, tool:)
|
|
150
356
|
|
|
357
|
+
role = Array(role)
|
|
358
|
+
|
|
151
359
|
@messages.each do |message|
|
|
152
360
|
role.include?(message.role) or next
|
|
153
361
|
!tool && message.tool? and next
|
|
@@ -305,14 +513,21 @@ class OllamaChat::MessageList
|
|
|
305
513
|
self
|
|
306
514
|
end
|
|
307
515
|
|
|
308
|
-
# Groups
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
# @
|
|
516
|
+
# Groups messages by their +group_uuid+, yielding an array of messages
|
|
517
|
+
# belonging to the same conversational turn (User -> Assistant -> Tools).
|
|
518
|
+
#
|
|
519
|
+
# @param role [Array<String>] the message roles to include when grouping.
|
|
520
|
+
# Defaults to +%w[user assistant]+. Pass +%w[user assistant tool]+
|
|
521
|
+
# to include tool-result messages as well.
|
|
522
|
+
# @param tool [Boolean] whether to include messages that carry a
|
|
523
|
+
# +tool_name+ (tool calls / responses) among +user+ / +assistant+ roles.
|
|
524
|
+
# Defaults to +false+.
|
|
525
|
+
# @yield [Array<OllamaChat::Message>] an array of messages sharing the
|
|
526
|
+
# same +group_uuid+.
|
|
312
527
|
# @return [Enumerator] if no block is given, returns an enumerator.
|
|
313
|
-
def each_group(&block)
|
|
314
|
-
block or return enum_for(__method__)
|
|
315
|
-
each_message.group_by(&:group_uuid).values.each(&block)
|
|
528
|
+
def each_group(role: %w[ user assistant ], tool: false, &block)
|
|
529
|
+
block or return enum_for(__method__, role:, tool:)
|
|
530
|
+
each_message(role:, tool:).group_by(&:group_uuid).values.each(&block)
|
|
316
531
|
end
|
|
317
532
|
|
|
318
533
|
# Removes the last `n` conversation exchanges from the message list.
|
|
@@ -411,8 +626,7 @@ class OllamaChat::MessageList
|
|
|
411
626
|
# @return [self, NilClass] nil if the system prompt is empty, otherwise self.
|
|
412
627
|
def show_system_prompt
|
|
413
628
|
current_system = system.to_s
|
|
414
|
-
|
|
415
|
-
es = OllamaChat::TokenEstimator.estimate(size_bytes)
|
|
629
|
+
es = OllamaChat::TokenEstimator.estimate(current_system)
|
|
416
630
|
system_prompt = @chat.kramdown_ansi_parse(current_system).
|
|
417
631
|
gsub(/\n+\z/, '').full?
|
|
418
632
|
if system_prompt.blank?
|
|
@@ -23,7 +23,7 @@ module OllamaChat::ModelHandling
|
|
|
23
23
|
# @attr_reader system [String] the system prompt associated with the model
|
|
24
24
|
# @attr_reader capabilities [Array<String>] the capabilities supported by the model
|
|
25
25
|
# @attr_reader families [Array<String>] the families of the model
|
|
26
|
-
class ModelMetadata <
|
|
26
|
+
class ModelMetadata < Data.define(:name, :system, :capabilities, :families)
|
|
27
27
|
# Checks if the given capability is included in the object's capabilities.
|
|
28
28
|
#
|
|
29
29
|
# @param capability [String] the capability to check for
|
data/lib/ollama_chat/oc.rb
CHANGED
|
@@ -158,14 +158,24 @@ module OC
|
|
|
158
158
|
default XDG_STATE_HOME + 'history.jsonl'
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
description '
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
module LOG
|
|
162
|
+
description 'Logging configuration'
|
|
163
|
+
|
|
164
|
+
CHAT = set do
|
|
165
|
+
description 'Chat log file path'
|
|
166
|
+
default XDG_STATE_HOME + 'chat.log'
|
|
167
|
+
end
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
DATABASE = set do
|
|
170
|
+
description 'Database log file path'
|
|
171
|
+
default XDG_STATE_HOME + 'database.log'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
TAIL_LINES = set do
|
|
175
|
+
description 'Max lines to keep in log files (truncate older)'
|
|
176
|
+
default 10_000
|
|
177
|
+
decode { Integer(_1) }
|
|
178
|
+
end
|
|
169
179
|
end
|
|
170
180
|
|
|
171
181
|
USER = set do
|
|
@@ -74,6 +74,7 @@ prompts:
|
|
|
74
74
|
markdown tables.
|
|
75
75
|
- **Session**: %{session_name}
|
|
76
76
|
- Context usage is %{context_usage}.
|
|
77
|
+
- Full conversation length is %{conversation_length}.
|
|
77
78
|
- Markdown output is %{markdown}. **Never** output markdown as your
|
|
78
79
|
responses if it is disabled.
|
|
79
80
|
- Voice output is %{voice}. Speak naturally and short if voice
|
|
@@ -170,10 +171,10 @@ prompts:
|
|
|
170
171
|
|
|
171
172
|
%{message_content}
|
|
172
173
|
session_title: |
|
|
173
|
-
Create a title with a length of **less than %{length}** characters for this
|
|
174
|
-
conversation. Output only the title and nothing else:
|
|
175
|
-
|
|
176
174
|
%{content}
|
|
175
|
+
|
|
176
|
+
Create a title with a length of **less than %{length}** characters for
|
|
177
|
+
the conversation above. Output only the title and nothing else.
|
|
177
178
|
context_template_suggest: |
|
|
178
179
|
Conversation History:
|
|
179
180
|
%{history}
|
|
@@ -341,6 +342,39 @@ prompts:
|
|
|
341
342
|
|
|
342
343
|
Output only the 9 suggestions as plain text, each separated by one empty line.
|
|
343
344
|
No other text.
|
|
345
|
+
compaction:
|
|
346
|
+
system: |
|
|
347
|
+
You are a context summarizer for a chat application.
|
|
348
|
+
Your sole task is to produce a concise, factual narrative
|
|
349
|
+
summary of the conversation provided. Be precise, omit
|
|
350
|
+
speculation, and follow the formatting instructions in the
|
|
351
|
+
user prompt exactly. Output only the summary text.
|
|
352
|
+
summarize: |
|
|
353
|
+
previous_summary:
|
|
354
|
+
%{previous}
|
|
355
|
+
|
|
356
|
+
conversation:
|
|
357
|
+
%{groups}
|
|
358
|
+
|
|
359
|
+
Summarize the conversation above. Your narrative MUST:
|
|
360
|
+
- Name every file created or significantly modified (with
|
|
361
|
+
path), and the key methods or classes they define.
|
|
362
|
+
- State the architectural or design decisions made (e.g.
|
|
363
|
+
data-model choices, API shape, error-handling strategy).
|
|
364
|
+
- Describe the current state: what works, what is pending,
|
|
365
|
+
and any open questions or follow-ups.
|
|
366
|
+
- Do NOT merely report test counts or coverage percentages;
|
|
367
|
+
those are outcomes, not accomplishments.
|
|
368
|
+
- Be factual; do not invent details not present in the groups
|
|
369
|
+
above. Keep it to 6–12 sentences.
|
|
370
|
+
assemble: |-
|
|
371
|
+
%{narrative}
|
|
372
|
+
|
|
373
|
+
tool_calls:
|
|
374
|
+
%{tool_section}
|
|
375
|
+
|
|
376
|
+
To see the full output of any tool call above, use lookup_group
|
|
377
|
+
with the group UUID in parentheses.
|
|
344
378
|
system:
|
|
345
379
|
default: <%= OC::OLLAMA::CHAT::SYSTEM || "%{persona}\n\n%{runtime_info}".inspect %>
|
|
346
380
|
persona: |
|
|
@@ -359,12 +393,22 @@ roles:
|
|
|
359
393
|
markdown: true
|
|
360
394
|
stream: true
|
|
361
395
|
document_policy: importing
|
|
396
|
+
parsing:
|
|
397
|
+
directory_structure_max_depth: 1
|
|
362
398
|
think:
|
|
363
399
|
mode: disabled
|
|
364
400
|
loud: true
|
|
365
401
|
strip: false
|
|
366
402
|
context:
|
|
367
403
|
format: JSON
|
|
404
|
+
compaction:
|
|
405
|
+
enabled: true
|
|
406
|
+
reserve:
|
|
407
|
+
ratio: 0.60
|
|
408
|
+
min_tokens: 4096
|
|
409
|
+
keep_recent:
|
|
410
|
+
ratio: 0.20
|
|
411
|
+
min_tokens: 2048
|
|
368
412
|
embedding:
|
|
369
413
|
enabled: true
|
|
370
414
|
paused: false
|
|
@@ -450,6 +494,10 @@ tools:
|
|
|
450
494
|
result_display_timeout: 3
|
|
451
495
|
cmd: |
|
|
452
496
|
grep #{'-i' if ignore_case} -m #{max_results} -r #{before.full? { "-B %u " % it }}#{after.full? { "-A %u " % it }}#{context.full? { "-C %u " % it }}#{pattern} #{path}
|
|
497
|
+
execute_shell:
|
|
498
|
+
default: false
|
|
499
|
+
require_confirmation: false # Preflight confirm is skipped, but in-tool it's mandatory.
|
|
500
|
+
result_display_timeout: 0
|
|
453
501
|
browse:
|
|
454
502
|
result_display_timeout: 3
|
|
455
503
|
default: true
|
|
@@ -492,6 +540,10 @@ tools:
|
|
|
492
540
|
gem_path_lookup:
|
|
493
541
|
result_display_timeout: 3
|
|
494
542
|
default: true
|
|
543
|
+
lookup_group:
|
|
544
|
+
default: true
|
|
545
|
+
require_confirmation: false
|
|
546
|
+
result_display_timeout: 3
|
|
495
547
|
open_file_in_editor:
|
|
496
548
|
default: true
|
|
497
549
|
require_confirmation: false
|
|
@@ -561,3 +613,20 @@ tools:
|
|
|
561
613
|
eval_ruby:
|
|
562
614
|
default: false
|
|
563
615
|
result_display_timeout: 3
|
|
616
|
+
syntax_checkers:
|
|
617
|
+
ruby:
|
|
618
|
+
cmd: [ 'ruby', '-wc' ]
|
|
619
|
+
suffixes: [ 'rb', 'rake', 'gemspec' ]
|
|
620
|
+
enabled: true
|
|
621
|
+
javascript:
|
|
622
|
+
cmd: [ 'node', '--check' ]
|
|
623
|
+
suffixes: [ 'js', 'mjs' ]
|
|
624
|
+
enabled: false
|
|
625
|
+
python:
|
|
626
|
+
cmd: [ 'python3', '-m', 'py_compile' ]
|
|
627
|
+
suffixes: [ 'py' ]
|
|
628
|
+
enabled: false
|
|
629
|
+
shell:
|
|
630
|
+
cmd: [ 'bash', '-n' ]
|
|
631
|
+
suffixes: [ 'sh', 'bash' ]
|
|
632
|
+
enabled: true
|
data/lib/ollama_chat/parsing.rb
CHANGED
|
@@ -279,7 +279,8 @@ module OllamaChat::Parsing
|
|
|
279
279
|
contents = [ content ]
|
|
280
280
|
content.scan(CONTENT_REGEXP).each { |url, file_url, quoted_file, file|
|
|
281
281
|
if file && Pathname.new(file).expand_path.directory?
|
|
282
|
-
|
|
282
|
+
max_depth = config.parsing.directory_structure_max_depth
|
|
283
|
+
contents << generate_structure(file, max_depth:).to_json
|
|
283
284
|
next
|
|
284
285
|
end
|
|
285
286
|
check_exist = false
|
|
@@ -23,8 +23,6 @@ module OllamaChat::RAGHandling
|
|
|
23
23
|
@documents.collection = old_collection
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
private
|
|
27
|
-
|
|
28
26
|
# Looks up a collection in the database by name.
|
|
29
27
|
#
|
|
30
28
|
# @param collection [String, Symbol, #to_s] the collection name to look up
|
|
@@ -34,6 +32,8 @@ module OllamaChat::RAGHandling
|
|
|
34
32
|
models::Collection[name: collection.to_s]
|
|
35
33
|
end
|
|
36
34
|
|
|
35
|
+
private
|
|
36
|
+
|
|
37
37
|
# Returns the name of the currently active document collection.
|
|
38
38
|
#
|
|
39
39
|
# @return [String, Symbol] the name of the current collection
|
|
@@ -160,12 +160,13 @@ module OllamaChat::RAGHandling
|
|
|
160
160
|
# highlighting the active one.
|
|
161
161
|
def list_collections
|
|
162
162
|
current_collection = collection.to_s
|
|
163
|
-
collections = all_collections.select(:name, :description)
|
|
163
|
+
collections = all_collections.select(:name, :description, :enabled)
|
|
164
164
|
use_pager do |output|
|
|
165
165
|
collections.each { |c|
|
|
166
166
|
collection_name = current_collection == c.name ? bold { c.name } : c.name
|
|
167
167
|
collection_description = c.description
|
|
168
|
-
|
|
168
|
+
suffix = c.enabled ? '' : ' [DISABLED]'
|
|
169
|
+
output.puts '%s: %s%s' % [ collection_name, collection_description, suffix ]
|
|
169
170
|
}
|
|
170
171
|
end
|
|
171
172
|
end
|
|
@@ -306,13 +307,26 @@ module OllamaChat::RAGHandling
|
|
|
306
307
|
extract_patterns(patterns_str)
|
|
307
308
|
end
|
|
308
309
|
|
|
310
|
+
enabled_prompt = col.enabled ?
|
|
311
|
+
"🚫 Disable this collection? (hidden from the model) (y/n) " :
|
|
312
|
+
"✅ Enable this collection? (visible to the model) (y/n) "
|
|
313
|
+
toggle = case confirm?(prompt: enabled_prompt)
|
|
314
|
+
when /\Ay/i then true
|
|
315
|
+
when /\An/i then false
|
|
316
|
+
end
|
|
317
|
+
|
|
309
318
|
col.description = new_description.full? ? new_description.to_s : col.description
|
|
310
319
|
col.patterns = patterns
|
|
320
|
+
col.enabled = toggle ^ col.enabled unless toggle.nil?
|
|
311
321
|
|
|
312
322
|
begin
|
|
313
323
|
col.save
|
|
324
|
+
if toggle
|
|
325
|
+
status = col.enabled ? 'enabled' : 'disabled'
|
|
326
|
+
STDOUT.puts "🔄 Collection '#{col.name}' is now #{status}."
|
|
327
|
+
end
|
|
314
328
|
STDOUT.puts "✅ Updated collection '#{col.name}'."
|
|
315
|
-
log(:info, "Collection updated", data: { name: col.name })
|
|
329
|
+
log(:info, "Collection updated", data: { name: col.name, enabled: col.enabled })
|
|
316
330
|
rescue Sequel::Error => e
|
|
317
331
|
STDERR.puts "❌ Database error: #{e.message}"
|
|
318
332
|
end
|
|
@@ -13,7 +13,7 @@ module OllamaChat::SessionManagement
|
|
|
13
13
|
output = StringIO.new
|
|
14
14
|
messages.write_conversation_jsonl(output)
|
|
15
15
|
session.update(messages: output.string)
|
|
16
|
-
es = session.estimate_tokens
|
|
16
|
+
es = session.estimate_tokens # We just use the formatted bytecount
|
|
17
17
|
log(:info, "Messages stored in session", data: {
|
|
18
18
|
session_id: session.id,
|
|
19
19
|
size: es.bytes_formatted,
|
|
@@ -135,9 +135,11 @@ module OllamaChat::SessionManagement
|
|
|
135
135
|
#
|
|
136
136
|
# @param output [IO] the output stream to write the information to (default: STDOUT)
|
|
137
137
|
def show_session(output: STDOUT)
|
|
138
|
-
es =
|
|
138
|
+
es = messages.full_estimate_tokens
|
|
139
139
|
messages_count = session.count_messages
|
|
140
|
-
output.puts "#{bold{session.name}} (#{italic{session.id}}),
|
|
140
|
+
output.puts "#{bold{session.name}} (#{italic{session.id}}), "\
|
|
141
|
+
"#{es.tokens_formatted} (#{es.bytes_formatted}), "\
|
|
142
|
+
"#{messages_count} messages"
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
# Interactively prompts the user for a unique session name.
|
|
@@ -7,7 +7,7 @@ class OllamaChat::TokenEstimator::Crude
|
|
|
7
7
|
# @raise [ArgumentError] if the input is not a string or an integer.
|
|
8
8
|
def initialize(arg)
|
|
9
9
|
if text = arg.ask_and_send(:to_str)
|
|
10
|
-
@bytes = text.
|
|
10
|
+
@bytes = text.bytesize
|
|
11
11
|
elsif bytes = arg.ask_and_send(:to_int)
|
|
12
12
|
@bytes = bytes
|
|
13
13
|
else
|
|
@@ -10,7 +10,7 @@ require 'ollama_chat/token_estimator/crude'
|
|
|
10
10
|
module OllamaChat::TokenEstimator
|
|
11
11
|
# Represents the result of a calculation including raw values
|
|
12
12
|
# and their human-readable formatted strings.
|
|
13
|
-
class Estimate <
|
|
13
|
+
class Estimate < Data.define(:bytes, :tokens)
|
|
14
14
|
include OllamaChat::Utils::ValueFormatter
|
|
15
15
|
|
|
16
16
|
# Returns the byte count in a formatted string (e.g., "1.2 KB").
|
|
@@ -29,6 +29,23 @@ module OllamaChat::Tools::Concern
|
|
|
29
29
|
#
|
|
30
30
|
# @return [ String ] the register name of the tool
|
|
31
31
|
attr_accessor :register_name
|
|
32
|
+
|
|
33
|
+
# Generates a one-line summary of a tool execution result for inclusion
|
|
34
|
+
# in compaction narratives.
|
|
35
|
+
#
|
|
36
|
+
# The default implementation extracts the `message` field from the
|
|
37
|
+
# result JSON (most tools already produce one). Falls back to a
|
|
38
|
+
# generic sentence if the field is absent or parsing fails.
|
|
39
|
+
#
|
|
40
|
+
# @param result [String] the raw JSON result string returned by `execute`
|
|
41
|
+
# @return [String] a short natural-language description of the call
|
|
42
|
+
def summary_template(result:)
|
|
43
|
+
default_message = "was called."
|
|
44
|
+
data = JSON.parse(result)
|
|
45
|
+
data.is_a?(Hash) && data['message'] || default_message
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
default_message
|
|
48
|
+
end
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
# The name method returns the registered name of the tool.
|
|
@@ -28,10 +28,10 @@ class OllamaChat::Tools::DirectoryStructure
|
|
|
28
28
|
description: <<~EOT,
|
|
29
29
|
Tree viewer – Returns JSON describing files/folders under path up to
|
|
30
30
|
max_depth (<= height of the tree), optionally only files ending with
|
|
31
|
-
suffix, e. g. rb for ruby files. Handy for locating
|
|
32
|
-
presenting a project layout. Limit the required tokens
|
|
33
|
-
max_depth parameter if possible, because the number of
|
|
34
|
-
tree can grow exponentially with its height.
|
|
31
|
+
suffix / file extension, e. g. rb for ruby files. Handy for locating
|
|
32
|
+
resources or presenting a project layout. Limit the required tokens
|
|
33
|
+
by using the max_depth parameter if possible, because the number of
|
|
34
|
+
nodes in a tree can grow exponentially with its height.
|
|
35
35
|
EOT
|
|
36
36
|
parameters: Tool::Function::Parameters.new(
|
|
37
37
|
type: 'object',
|
|
@@ -42,13 +42,17 @@ class OllamaChat::Tools::DirectoryStructure
|
|
|
42
42
|
),
|
|
43
43
|
suffix: Tool::Function::Parameters::Property.new(
|
|
44
44
|
type: 'string',
|
|
45
|
-
description:
|
|
45
|
+
description: <<~EOT
|
|
46
|
+
Only include files with this suffix / file extension, e. g.
|
|
47
|
+
"rb" (defaults to all)
|
|
48
|
+
EOT
|
|
46
49
|
),
|
|
47
50
|
max_depth: Tool::Function::Parameters::Property.new(
|
|
48
51
|
type: 'integer',
|
|
49
52
|
description: <<~EOT,
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
How many levels of the tree to include. 1 = immediate
|
|
54
|
+
children only, 2 = children + grandchildren, nil =
|
|
55
|
+
unlimited (defaults to nil)
|
|
52
56
|
EOT
|
|
53
57
|
),
|
|
54
58
|
},
|
|
@@ -7,6 +7,7 @@ require 'open3'
|
|
|
7
7
|
# It provides a safe way to test snippets and verify Ruby behavior.
|
|
8
8
|
class OllamaChat::Tools::EvalRuby
|
|
9
9
|
include OllamaChat::Tools::Concern
|
|
10
|
+
include Kramdown::ANSI::Width
|
|
10
11
|
|
|
11
12
|
# @return [String] the registered name for this tool
|
|
12
13
|
def self.register_name = 'eval_ruby'
|
|
@@ -79,18 +80,27 @@ class OllamaChat::Tools::EvalRuby
|
|
|
79
80
|
result = stdout&.sub(/\ASwitch to inspect mode.\n/, '')
|
|
80
81
|
|
|
81
82
|
if status.success?
|
|
83
|
+
result_truncated = truncate(result.to_s.strip, length: 80)
|
|
84
|
+
message = "eval_ruby succeeded (#{version}): #{result_truncated.inspect}"
|
|
82
85
|
{
|
|
83
86
|
result:,
|
|
84
87
|
version:,
|
|
85
|
-
status: 'success'
|
|
88
|
+
status: 'success',
|
|
89
|
+
message: ,
|
|
86
90
|
}.to_json
|
|
87
91
|
else
|
|
92
|
+
message =
|
|
93
|
+
if stderr.empty?
|
|
94
|
+
"eval_ruby failed (#{version}): exit status #{status.exitstatus}"
|
|
95
|
+
else
|
|
96
|
+
"eval_ruby failed (#{version}): #{truncate(stderr.to_s.strip, length: 80).inspect}"
|
|
97
|
+
end
|
|
88
98
|
{
|
|
89
|
-
error:
|
|
90
|
-
message:
|
|
91
|
-
version
|
|
92
|
-
stdout
|
|
93
|
-
exit_status: status.exitstatus
|
|
99
|
+
error: 'ExecutionError',
|
|
100
|
+
message: ,
|
|
101
|
+
version: ,
|
|
102
|
+
stdout: ,
|
|
103
|
+
exit_status: status.exitstatus,
|
|
94
104
|
}.to_json
|
|
95
105
|
end
|
|
96
106
|
rescue => e
|