legion-llm 0.9.15 → 0.9.17

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: 206afbe8609bb8ed7df111d216967aafba55b0d523d5939aad024f169e43f5ef
4
- data.tar.gz: 283f42c3d5b9ba07aa7857aad3e6d1f30b7559f35282f4a1d00986ea4ab2c646
3
+ metadata.gz: ce44e3d928a848ab67e5cd50574c7454ff3490a455c1d040c7089641e1091e5e
4
+ data.tar.gz: ef3eaa05c9340b08f94af99c7b4f35334cef2a1d8dd09aeafb3535532840b4ce
5
5
  SHA512:
6
- metadata.gz: 54b3e821013f9ba6f73019907821e85d1aaacc766e8942767f5e6a9630d66757c1d16a8e1b6643054895b1e5de229245e45ebf562f42bdec1cfac8f609024a5c
7
- data.tar.gz: 45d349d01bef14e68527aa0c8108c4d08f71e05b63c87c331e377703bdacfcee431b903fe72dc2ad48b10c2f73a67523e9c67a0da1dd46b4b9cf3e041c290671
6
+ metadata.gz: 1dc635c864ac647911bc6d55a34209f10273471b63683ab4eaa7dc69fdee7d3047c6b028b9d5180688b0b7ed4624c89fa90e5a7745f2462cb30371fe84607a11
7
+ data.tar.gz: ddd2e32d57a9a56d1fff22c4b7e423145d743183efd7044290482612b704b4c787db9790d28733df53e69a90ed57e4af1d7f6bef89f069ad3a25fafbe09b63ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.9.17] - 2026-05-11
4
+
5
+ ### Fixed
6
+ - `total_memory_mb` now fetched exactly once on first access and never re-fetched; hardware memory is static so repeated `sysctl` calls every 60s were wasteful. `refresh!` only clears the available memory cache; `reset!` still clears everything (for tests).
7
+ - `trivial_query?` now correctly identifies short/trivial messages: a query is trivial if it matches a known trivial pattern (exact normalized match), or if no custom patterns are configured and the query is short (under `trivial_max_chars`) and a single word. Previously, an empty patterns list caused `.any?` to always return false, so nothing was ever trivial.
8
+ - Added `trivial_patterns` helper with configurable defaults (`ping`, `pong`, `ding`, `test`, `foobar`) readable via `rag.trivial_patterns` setting; when custom patterns are explicitly configured, the short-query heuristic is disabled so only listed patterns are treated as trivial.
9
+
10
+ ## [0.9.16] - 2026-05-11
11
+
12
+ ### Fixed
13
+ - Renamed `Metering#settings_value` to `extract_hash_value` to fix method shadowing with `Legion::Logging::Helper#settings_value`, which resolves a `wrong number of arguments (given 3, expected 2)` error raised from `instance_log_level` when metering is active.
14
+
3
15
  ## [0.9.15] - 2026-05-08
4
16
 
5
17
  ### Fixed
@@ -247,11 +247,13 @@ module Legion
247
247
  def load_curated(conversation_id)
248
248
  return nil unless Inference::Conversation.conversation_exists?(conversation_id)
249
249
 
250
- raw = Inference::Conversation.messages(conversation_id)
250
+ # Use raw_messages so CURATED_ROLE entries are visible even though they
251
+ # are filtered out of the public-facing Conversation#messages array.
252
+ raw = Inference::Conversation.raw_messages(conversation_id)
251
253
  curated_entries = raw.select { |m| m[:role] == CURATED_KEY }
252
254
  return nil if curated_entries.empty?
253
255
 
254
- regular = raw.reject { |m| m[:role] == CURATED_KEY }
256
+ regular = raw.reject { |m| [CURATED_KEY, Inference::Conversation::METADATA_ROLE].include?(m[:role]) }
255
257
  summaries = normalized_curated_summaries(curated_entries)
256
258
  if summaries.empty?
257
259
  apply_curation_pipeline(regular)
@@ -31,9 +31,7 @@ module Legion
31
31
  end
32
32
 
33
33
  def refresh!
34
- @total_fetched_at = nil
35
34
  @available_fetched_at = nil
36
- @total_memory_mb = nil
37
35
  @available_memory_mb = nil
38
36
  @last_refreshed_at = Time.now
39
37
  end
@@ -57,7 +55,6 @@ module Legion
57
55
  private
58
56
 
59
57
  def ensure_total_fresh
60
- refresh! if stale?
61
58
  return unless @total_fetched_at.nil?
62
59
 
63
60
  fetch_total
@@ -11,6 +11,7 @@ module Legion
11
11
 
12
12
  MAX_CONVERSATIONS = 256
13
13
  METADATA_ROLE = :__metadata__
14
+ CURATED_ROLE = :__curated__
14
15
 
15
16
  class << self
16
17
  def append(conversation_id, role:, content:, parent_id: nil, sidechain: false,
@@ -38,29 +39,41 @@ module Legion
38
39
 
39
40
  # Returns flat ordered message array — backward-compatible.
40
41
  # Uses chain reconstruction when parent links exist; falls back to seq order.
42
+ # Internal-only roles (__metadata__, __curated__) are filtered out.
41
43
  def messages(conversation_id)
42
44
  if in_memory?(conversation_id)
43
45
  touch(conversation_id)
44
- raw = conversations[conversation_id][:messages].reject { |m| m[:role] == METADATA_ROLE }
46
+ raw = conversations[conversation_id][:messages].reject { |m| internal_role?(m[:role]) }
45
47
  chain_or_seq(raw)
46
48
  else
47
49
  load_from_db(conversation_id)
48
50
  end
49
51
  end
50
52
 
53
+ # Returns ALL messages including internal-role entries (__metadata__, __curated__).
54
+ # Use this when you need access to curation markers or metadata entries.
55
+ def raw_messages(conversation_id)
56
+ if in_memory?(conversation_id)
57
+ touch(conversation_id)
58
+ conversations[conversation_id][:messages].dup
59
+ else
60
+ load_all_from_db(conversation_id)
61
+ end
62
+ end
63
+
51
64
  # Build ordered chain from parent links.
52
65
  # Excludes sidechain messages by default.
53
66
  def build_chain(conversation_id, include_sidechains: false)
54
67
  raw = all_raw_messages(conversation_id)
55
68
  raw = raw.reject { |m| m[:sidechain] } unless include_sidechains
56
- raw = raw.reject { |m| m[:role] == METADATA_ROLE }
69
+ raw = raw.reject { |m| internal_role?(m[:role]) }
57
70
  reconstruct_chain(raw)
58
71
  end
59
72
 
60
73
  # Return sidechain messages; optionally filter by agent_id.
61
74
  def sidechain_messages(conversation_id, agent_id: nil)
62
75
  raw = all_raw_messages(conversation_id)
63
- result = raw.select { |m| m[:sidechain] && m[:role] != METADATA_ROLE }
76
+ result = raw.select { |m| m[:sidechain] && !internal_role?(m[:role]) }
64
77
  result = result.select { |m| m[:agent_id] == agent_id } unless agent_id.nil?
65
78
  result.sort_by { |m| m[:seq] }
66
79
  end
@@ -243,6 +256,12 @@ module Legion
243
256
 
244
257
  private
245
258
 
259
+ # Returns true for roles that are internal bookkeeping and should not
260
+ # appear in the public-facing message array returned by #messages.
261
+ def internal_role?(role)
262
+ [METADATA_ROLE, CURATED_ROLE].include?(role)
263
+ end
264
+
246
265
  def conversations
247
266
  @conversations ||= {}
248
267
  end
@@ -543,9 +562,22 @@ module Legion
543
562
  .where(conversation_id: conversation_id)
544
563
  .order(:seq)
545
564
  .map { |row| symbolize_message(row) }
565
+ .reject { |m| internal_role?(m[:role]) }
546
566
  chain_or_seq(rows)
547
567
  end
548
568
 
569
+ def load_all_from_db(conversation_id)
570
+ return [] unless db_available?
571
+
572
+ Legion::Data.connection[:conversation_messages]
573
+ .where(conversation_id: conversation_id)
574
+ .order(:seq)
575
+ .map { |row| symbolize_message(row) }
576
+ rescue StandardError => e
577
+ handle_exception(e, level: :debug)
578
+ []
579
+ end
580
+
549
581
  def db_conversation_record?(conversation_id)
550
582
  Legion::Data.connection[:conversations].where(id: conversation_id).any?
551
583
  end
@@ -134,12 +134,18 @@ module Legion
134
134
  def trivial_query?(query)
135
135
  query = content_text(query)
136
136
  max_chars = rag_setting(:trivial_max_chars, 20)
137
- patterns = rag_setting(:trivial_patterns, [])
138
-
139
- return false if query.length > max_chars
137
+ configured_patterns = rag_setting(:trivial_patterns)
140
138
 
141
139
  normalized = query.strip.downcase.gsub(/[^a-z0-9\s]/, '')
142
- patterns.any? { |p| normalized == p }
140
+ patterns = configured_patterns || trivial_patterns
141
+ return true if patterns.any? { |p| normalized == p }
142
+ return true if configured_patterns.nil? && query.length <= max_chars && normalized.split.length <= 1
143
+
144
+ false
145
+ end
146
+
147
+ def trivial_patterns
148
+ rag_setting(:trivial_patterns, %w[ping pong ding test foobar])
143
149
  end
144
150
 
145
151
  def apollo_available?
@@ -314,7 +320,8 @@ module Legion
314
320
  def positive_integer(value)
315
321
  integer = Integer(value)
316
322
  integer.positive? ? integer : nil
317
- rescue ArgumentError, TypeError
323
+ rescue ArgumentError, TypeError => e
324
+ handle_exception(e, level: :debug, handled: true, operation: 'llm.pipeline.steps.rag_context.positive_integer')
318
325
  nil
319
326
  end
320
327
  end
@@ -142,26 +142,26 @@ module Legion
142
142
  def extract_usage(response)
143
143
  return { input_tokens: 0, output_tokens: 0 } unless response.is_a?(Hash)
144
144
 
145
- usage = settings_value(response, :usage) || {}
145
+ usage = extract_hash_value(response, :usage) || {}
146
146
  {
147
- input_tokens: settings_value(usage, :input_tokens) || settings_value(usage, :prompt_tokens) || 0,
148
- output_tokens: settings_value(usage, :output_tokens) || settings_value(usage, :completion_tokens) || 0
147
+ input_tokens: extract_hash_value(usage, :input_tokens) || extract_hash_value(usage, :prompt_tokens) || 0,
148
+ output_tokens: extract_hash_value(usage, :output_tokens) || extract_hash_value(usage, :completion_tokens) || 0
149
149
  }
150
150
  end
151
151
 
152
152
  def extract_provider(response)
153
153
  return nil unless response.is_a?(Hash)
154
154
 
155
- settings_value(settings_value(response, :meta), :provider) || settings_value(response, :provider)
155
+ extract_hash_value(extract_hash_value(response, :meta), :provider) || extract_hash_value(response, :provider)
156
156
  end
157
157
 
158
158
  def extract_model(response)
159
159
  return nil unless response.is_a?(Hash)
160
160
 
161
- settings_value(settings_value(response, :meta), :model) || settings_value(response, :model)
161
+ extract_hash_value(extract_hash_value(response, :meta), :model) || extract_hash_value(response, :model)
162
162
  end
163
163
 
164
- def settings_value(hash, key)
164
+ def extract_hash_value(hash, key)
165
165
  return nil unless hash.respond_to?(:key?)
166
166
 
167
167
  string_key = key.to_s
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.9.15'
5
+ VERSION = '0.9.17'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.15
4
+ version: 0.9.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity