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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/legion/llm/context/curator.rb +4 -2
- data/lib/legion/llm/discovery/system.rb +0 -3
- data/lib/legion/llm/inference/conversation.rb +35 -3
- data/lib/legion/llm/inference/steps/rag_context.rb +12 -5
- data/lib/legion/llm/metering.rb +6 -6
- data/lib/legion/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce44e3d928a848ab67e5cd50574c7454ff3490a455c1d040c7089641e1091e5e
|
|
4
|
+
data.tar.gz: ef3eaa05c9340b08f94af99c7b4f35334cef2a1d8dd09aeafb3535532840b4ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/legion/llm/metering.rb
CHANGED
|
@@ -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 =
|
|
145
|
+
usage = extract_hash_value(response, :usage) || {}
|
|
146
146
|
{
|
|
147
|
-
input_tokens:
|
|
148
|
-
output_tokens:
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
extract_hash_value(extract_hash_value(response, :meta), :model) || extract_hash_value(response, :model)
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
def
|
|
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
|
data/lib/legion/llm/version.rb
CHANGED