Module: OllamaChat::Compaction
- Included in:
- Chat
- Defined in:
- lib/ollama_chat/compaction.rb
Overview
Provides compaction support for chat sessions. Mixed into Chat.
Supplies configuration resolvers that derive concrete token budgets
from the compaction: config block (ratios of num_ctx with absolute
floors), the LLM-based summarization pipeline
(+summarize_for_compaction+), and the tool_summary_line resolver
for per-tool summary templates.
Defined Under Namespace
Classes: Result
Class Method Summary collapse
-
.tool_summary_line(tool_name, result) ⇒ String
Generates a one-line summary for a tool call result.
Instance Method Summary collapse
-
#assemble_summary(narrative, tool_entries, old_tool_entries = []) ⇒ Array(String, Array<Hash>)
private
Assembles the final summary content and merged tool entries.
-
#build_tool_entries(messages) ⇒ Array<Hash>
private
Builds the deterministic tool-call entries from tool result messages.
-
#call_summarizer(groups:, previous_summary:) ⇒ String
private
Calls the LLM to generate the narrative portion of the summary.
-
#compact_min_tokens(name) ⇒ Integer
private
Returns the absolute minimum token floor for a given concern.
-
#compact_ratio(name) ⇒ Float
private
Returns the fractional ratio (portion of
num_ctx) for a given concern. -
#compact_ratio_tokens(name, tokens) ⇒ Integer
Resolves the effective token budget for a concern given a context size.
-
#compact_with_retry ⇒ Boolean
Attempts compaction, prompting the user to retry on failure.
-
#extract_narrative(content) ⇒ String
private
Extracts the narrative portion from a summary message's content.
-
#extract_old_tool_calls(summary_msg) ⇒ Array<Hash>
private
Returns the tool-call entries from a previous summary message.
-
#format_summary_line(msg) ⇒ String
private
Formats a single message as a summary line.
-
#report_compaction(result) ⇒ Object
private
Displays a compact compaction report to
STDOUTafter theInfobar.busyspinner has finished. -
#serialize_groups(messages) ⇒ String
private
Serializes non-system messages into a readable block for the prompt.
-
#summarize_for_compaction(messages:, previous_summary: nil) ⇒ Array(String, Array<Hash>)
Generates a context summary for the given messages via LLM.
Class Method Details
.tool_summary_line(tool_name, result) ⇒ String
Generates a one-line summary for a tool call result.
Looks up the registered tool class and delegates to its
summary_template(result:) class method. Falls back to a generic
sentence if the tool is not registered or the template raises.
22 23 24 25 26 27 28 |
# File 'lib/ollama_chat/compaction.rb', line 22 def tool_summary_line(tool_name, result) default_summary = "was called." klass = OllamaChat::Tools.registered[tool_name.to_s]&.class klass&.summary_template(result:) || default_summary rescue StandardError default_summary end |
Instance Method Details
#assemble_summary(narrative, tool_entries, old_tool_entries = []) ⇒ Array(String, Array<Hash>) (private)
Assembles the final summary content and merged tool entries.
229 230 231 232 233 234 235 236 237 |
# File 'lib/ollama_chat/compaction.rb', line 229 def assemble_summary(narrative, tool_entries, old_tool_entries = []) all_tools = old_tool_entries + tool_entries tool_section = all_tools.map(&:to_json) * ?\n template = prompt(:assemble, context: 'compaction').to_s content = template % { narrative:, tool_section: } [content, all_tools] end |
#build_tool_entries(messages) ⇒ Array<Hash> (private)
Builds the deterministic tool-call entries from tool result messages.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/ollama_chat/compaction.rb', line 164 def build_tool_entries() .filter_map do |msg| next unless tool_name = msg.tool_name.full? next if tool_name == 'runtime_information' uuid = msg.group_uuid.to_s[-8..] summary = OllamaChat::Compaction.tool_summary_line( tool_name, msg.content.to_s ) time = msg.group_time&.strftime('%Y-%m-%d %H:%M') { 'type' => 'tool', 'name' => tool_name, 'summary' => summary, 'uuid' => uuid, 'time' => time, } end end |
#call_summarizer(groups:, previous_summary:) ⇒ String (private)
Calls the LLM to generate the narrative portion of the summary.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/ollama_chat/compaction.rb', line 188 def call_summarizer(groups:, previous_summary:) previous = previous_summary.to_s prompt = prompt(:summarize, context: 'compaction').to_s % { previous:, groups: } system = prompt(:compaction, context: 'system').to_s es = OllamaChat::TokenEstimator.estimate(prompt) log(:info, "Compaction: sending prompt " \ "#{es.tokens_formatted} (#{es.bytes_formatted}) to #{@model}") response = Infobar.busy( label: 'Compacting context…', frames: :braille7, output: STDOUT, ) do generate(system:, prompt:, think: true).strip end if response.empty? log(:error, 'Compaction: LLM returned empty response') raise OllamaChat::CompactionError, 'Summarization failed: model returned empty response' end res_es = OllamaChat::TokenEstimator.estimate(response) log(:info, "Compaction: got response " \ "#{res_es.tokens_formatted} (#{res_es.bytes_formatted})") response end |
#compact_min_tokens(name) ⇒ Integer (private)
Returns the absolute minimum token floor for a given concern.
260 261 262 |
# File 'lib/ollama_chat/compaction.rb', line 260 def compact_min_tokens(name) config.compaction.attribute_get!(name).min_tokens end |
#compact_ratio(name) ⇒ Float (private)
Returns the fractional ratio (portion of num_ctx) for a given concern.
269 270 271 |
# File 'lib/ollama_chat/compaction.rb', line 269 def compact_ratio(name) config.compaction.attribute_get!(name).ratio end |
#compact_ratio_tokens(name, tokens) ⇒ Integer
Resolves the effective token budget for a concern given a context size.
Computes max(ratio * tokens, min_tokens).floor, so the floor acts as
a guaranteed minimum even for very small context windows.
91 92 93 94 95 96 |
# File 'lib/ollama_chat/compaction.rb', line 91 def compact_ratio_tokens(name, tokens) [ compact_ratio(name).to_f * tokens, compact_min_tokens(name).to_f, ].max.floor end |
#compact_with_retry ⇒ Boolean
Attempts compaction, prompting the user to retry on failure.
compact! is idempotent on failure: the LLM call (and thus any
CompactionError) happens before the message list is mutated, so a
retry always starts from a clean state.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/ollama_chat/compaction.rb', line 106 def compact_with_retry result = .compact! session_sync if result report_compaction(result) else STDOUT.puts('Nothing to compact.') end return true rescue OllamaChat::CompactionError => e STDERR.puts "⚠️ Compaction failed: #{e.}" log(:error, "Compaction failed", data: { model: @model, ctx: current_context_length, size: .size, }) retry if confirm?( prompt: '🔔 Retry compaction? (y/n) ', yes: /\Ay/i ) false end |
#extract_narrative(content) ⇒ String (private)
Extracts the narrative portion from a summary message's content.
243 244 245 |
# File 'lib/ollama_chat/compaction.rb', line 243 def extract_narrative(content) content.to_s.sub(/\ntool_calls:\n.*\z/m, '').strip end |
#extract_old_tool_calls(summary_msg) ⇒ Array<Hash> (private)
Returns the tool-call entries from a previous summary message.
251 252 253 |
# File 'lib/ollama_chat/compaction.rb', line 251 def extract_old_tool_calls(summary_msg) summary_msg.tool_calls || [] end |
#format_summary_line(msg) ⇒ String (private)
Formats a single message as a summary line.
146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/ollama_chat/compaction.rb', line 146 def format_summary_line(msg) tool_name = msg.tool_name.full? parts = +"[#{msg.role}]" parts << " (#{tool_name})" if tool_name parts << " (g:#{msg.group_uuid.to_s[-8..]})" if msg.group_uuid if tool_name line = OllamaChat::Compaction.tool_summary_line(tool_name, msg.content.to_s) parts << " #{line}" elsif msg.content.present? parts << " #{msg.content.to_s.strip}" end parts end |
#report_compaction(result) ⇒ Object (private)
Displays a compact compaction report to STDOUT after the
Infobar.busy spinner has finished.
Replaces the former bare "Conversation compacted." line with before/after context metrics, candidate volume, and summary size.
280 281 282 283 284 285 286 287 288 |
# File 'lib/ollama_chat/compaction.rb', line 280 def report_compaction(result) STDOUT.puts(<<~EOT) ✅ Conversation compacted. Summarized: #{result.candidates} messages (#{result.candidate_size}) Summary: #{result.summary_size} Context: #{result.context_before} → #{result.context_after} Stored total: #{result.stored_total} EOT end |
#serialize_groups(messages) ⇒ String (private)
Serializes non-system messages into a readable block for the prompt.
135 136 137 138 139 140 |
# File 'lib/ollama_chat/compaction.rb', line 135 def serialize_groups() .reject { _1.role == 'system' } .map { |m| format_summary_line(m) } .join("\n") end |
#summarize_for_compaction(messages:, previous_summary: nil) ⇒ Array(String, Array<Hash>)
Generates a context summary for the given messages via LLM.
Serializes non-system messages into a structured prompt, calls the
model via generate, and assembles the final summary with
deterministic tool-call entries stored out-of-band.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/ollama_chat/compaction.rb', line 62 def summarize_for_compaction(messages:, previous_summary: nil) groups = serialize_groups() tool_entries = build_tool_entries() es = OllamaChat::TokenEstimator.estimate(groups) log(:info, "Compaction: #{.size} messages, " \ "#{es.tokens_formatted} (#{es.bytes_formatted}) groups") narrative_prev = '' old_tool_calls = [] if previous_summary narrative_prev = extract_narrative(previous_summary.content) old_tool_calls = extract_old_tool_calls(previous_summary) end narrative = call_summarizer(groups:, previous_summary: narrative_prev) assemble_summary(narrative, tool_entries, old_tool_calls) end |