Class: Legion::LLM::ContextCurator
- Inherits:
-
Object
- Object
- Legion::LLM::ContextCurator
- Includes:
- Legion::Logging::Helper
- Defined in:
- lib/legion/llm/context_curator.rb
Constant Summary collapse
- CURATED_KEY =
:__curated__
Instance Method Summary collapse
-
#curate_turn(turn_messages:, assistant_response:) ⇒ Object
Called async after each turn completes — zero latency impact.
-
#curated_messages ⇒ Object
Called sync when building next API request.
-
#dedup_similar(messages, threshold: nil) ⇒ Object
Heuristic: deduplicate near-identical messages using Jaccard similarity.
-
#distill_tool_result(msg, _assistant_context = nil) ⇒ Object
Heuristic: distill a single tool-result message to a compact summary.
-
#evict_superseded(messages) ⇒ Object
Heuristic: if same file was read multiple times, keep only the latest read.
-
#fold_resolved_exchanges(messages) ⇒ Object
Heuristic: detect multi-turn clarification that reached agreement; fold to single system note.
-
#initialize(conversation_id:) ⇒ ContextCurator
constructor
A new instance of ContextCurator.
-
#llm_distill_tool_result(msg, assistant_response = nil) ⇒ Object
LLM-assisted distillation: uses small/fast model to summarize tool results.
-
#strip_thinking(msg) ⇒ Object
Heuristic: remove extended thinking blocks, keep conclusions.
Constructor Details
#initialize(conversation_id:) ⇒ ContextCurator
Returns a new instance of ContextCurator.
11 12 13 14 |
# File 'lib/legion/llm/context_curator.rb', line 11 def initialize(conversation_id:) @conversation_id = conversation_id @curated_cache = nil end |
Instance Method Details
#curate_turn(turn_messages:, assistant_response:) ⇒ Object
Called async after each turn completes — zero latency impact.
17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/legion/llm/context_curator.rb', line 17 def curate_turn(turn_messages:, assistant_response:) return unless enabled? Thread.new do curated = .map { |msg| (msg, assistant_response) } store_curated(@conversation_id, curated) @curated_cache = nil rescue StandardError => e handle_exception(e, level: :warn) end end |
#curated_messages ⇒ Object
Called sync when building next API request. Returns curated messages when available; nil means use raw history.
31 32 33 34 35 |
# File 'lib/legion/llm/context_curator.rb', line 31 def return nil unless enabled? @curated_messages ||= load_curated(@conversation_id) end |
#dedup_similar(messages, threshold: nil) ⇒ Object
Heuristic: deduplicate near-identical messages using Jaccard similarity.
105 106 107 108 109 110 111 |
# File 'lib/legion/llm/context_curator.rb', line 105 def dedup_similar(, threshold: nil) return unless setting(:dedup_enabled, true) threshold ||= setting(:dedup_threshold, 0.85) result = Compressor.(, threshold: threshold) result[:messages] end |
#distill_tool_result(msg, _assistant_context = nil) ⇒ Object
Heuristic: distill a single tool-result message to a compact summary.
38 39 40 41 42 43 44 45 |
# File 'lib/legion/llm/context_curator.rb', line 38 def distill_tool_result(msg, _assistant_context = nil) content = msg[:content].to_s max_chars = setting(:tool_result_max_chars, 2000) return msg if content.length <= max_chars summary = heuristic_tool_summary(content, tool_name_from(msg)) msg.merge(content: summary, curated: true, original_content: content) end |
#evict_superseded(messages) ⇒ Object
Heuristic: if same file was read multiple times, keep only the latest read.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/legion/llm/context_curator.rb', line 89 def evict_superseded() return unless setting(:superseded_eviction, true) file_last_seen = {} .each_with_index do |msg, idx| path = extract_file_path(msg[:content].to_s) file_last_seen[path] = idx if path end .each_with_index.reject do |msg, idx| path = extract_file_path(msg[:content].to_s) path && file_last_seen[path] != idx end.map(&:first) end |
#fold_resolved_exchanges(messages) ⇒ Object
Heuristic: detect multi-turn clarification that reached agreement; fold to single system note.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/legion/llm/context_curator.rb', line 63 def fold_resolved_exchanges() return unless setting(:exchange_folding, true) result = [] i = 0 while i < .length window = [i, 4] if resolved_exchange?(window) conclusion = window.last[:content].to_s[0, 300] note = { role: :system, content: "[Exchange resolved: #{conclusion}]", curated: true, original_content: window.map { |m| m[:content] }.join("\n") } result << note i += window.length else result << [i] i += 1 end end result end |
#llm_distill_tool_result(msg, assistant_response = nil) ⇒ Object
LLM-assisted distillation: uses small/fast model to summarize tool results. Falls back to heuristic on any error.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/legion/llm/context_curator.rb', line 115 def llm_distill_tool_result(msg, assistant_response = nil) return distill_tool_result(msg, assistant_response) unless llm_assisted? content = msg[:content].to_s max_chars = setting(:tool_result_max_chars, 2000) return msg if content.length <= max_chars summary = llm_summarize_tool_result(content, tool_name_from(msg)) if summary msg.merge(content: summary, curated: true, original_content: content) else distill_tool_result(msg, assistant_response) end rescue StandardError => e handle_exception(e, level: :warn) distill_tool_result(msg, assistant_response) end |
#strip_thinking(msg) ⇒ Object
Heuristic: remove extended thinking blocks, keep conclusions.
48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/legion/llm/context_curator.rb', line 48 def strip_thinking(msg) return msg unless setting(:thinking_eviction, true) content = msg[:content].to_s stripped = content .gsub(%r{<thinking>.*?</thinking>}m, '') .gsub(/^#+\s*[Tt]hinking.*?\n(?:(?!^#+\s).)*\n/m, '') .strip return msg if stripped == content || stripped.empty? msg.merge(content: stripped, curated: true, original_content: content) end |