lex-apollo 0.4.20 → 0.4.22
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 +10 -0
- data/lib/legion/extensions/apollo/api.rb +32 -12
- data/lib/legion/extensions/apollo/runners/knowledge.rb +23 -3
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/spec/legion/extensions/apollo/api_spec.rb +64 -0
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +30 -0
- 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: 9a3e0ff5897e31c3f8e91f5b71eb96fd9ee90a15a041a5034ae936e2188806ae
|
|
4
|
+
data.tar.gz: c3aba4e4e156c940298dde9c54da46b77e87ffa05db60b7f4b3877307fa20c1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8d3000ec6c34b23c5f49eea11b2a03d8ef117d395f9134782cf1c2fe91650b569b036571aaafd023d36d3fbd74725d492e44b98ed701348717b3221a7e45986
|
|
7
|
+
data.tar.gz: 811fd837e8370dde5d998daadad819f648da2f4b1aae3258c82573efb773a4901c6aff8b6cbcbe60eb9f964cee9146b81288d923429321cc7e70e046cd14371b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.22] - 2026-04-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `/api/apollo/stats` now returns the health UI metrics Interlink expects: `recent_24h`, `avg_confidence`, and a synthesized `by_status["active"]` count for non-archived entries. This keeps the Knowledge Health cards populated instead of rendering missing values. Fixes #16.
|
|
7
|
+
|
|
8
|
+
## [0.4.21] - 2026-04-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `Apollo::Runners::Knowledge#handle_ingest` now emits warn-level logs on the three early-return failure paths (nil/blank content, nil content_type, apollo_data_not_available). Companion to PR #15: that PR added `handle_exception` to the rescue paths; this PR closes the silent-failure window for the early-return paths that fire BEFORE any rescue would. Tag values in the log line are sanitized via `gsub(/[\r\n]+/, ' ')` to prevent log-line injection from caller-controlled tags.
|
|
12
|
+
|
|
3
13
|
## [0.4.20] - 2026-04-25
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -9,6 +9,37 @@ module Legion
|
|
|
9
9
|
class Api < Sinatra::Base
|
|
10
10
|
set :host_authorization, permitted: :any
|
|
11
11
|
|
|
12
|
+
class << self
|
|
13
|
+
def stats_payload(now: Time.now)
|
|
14
|
+
return { error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
15
|
+
|
|
16
|
+
entries = Legion::Data::Model::ApolloEntry
|
|
17
|
+
by_status = grouped_counts(entries, :status)
|
|
18
|
+
by_status['active'] = entries.exclude(status: 'archived').count
|
|
19
|
+
|
|
20
|
+
stats = {
|
|
21
|
+
total_entries: entries.count,
|
|
22
|
+
recent_24h: entries.where { created_at >= (now - 86_400) }.count,
|
|
23
|
+
avg_confidence: average_confidence(entries),
|
|
24
|
+
by_status: by_status,
|
|
25
|
+
by_content_type: grouped_counts(entries, :content_type)
|
|
26
|
+
}
|
|
27
|
+
stats[:total_relations] = Legion::Data::Model::ApolloRelation.count if defined?(Legion::Data::Model::ApolloRelation)
|
|
28
|
+
stats
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def grouped_counts(entries, column)
|
|
34
|
+
entries.group_and_count(column).all.to_h { |row| [row[column].to_s, row[:count]] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def average_confidence(entries)
|
|
38
|
+
avg = entries.avg(:confidence)
|
|
39
|
+
avg&.to_f&.round(3)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
12
43
|
before do
|
|
13
44
|
content_type :json
|
|
14
45
|
end
|
|
@@ -140,18 +171,7 @@ module Legion
|
|
|
140
171
|
|
|
141
172
|
# Statistics
|
|
142
173
|
get '/api/apollo/stats' do
|
|
143
|
-
|
|
144
|
-
if defined?(Legion::Data::Model::ApolloEntry)
|
|
145
|
-
stats[:total_entries] = Legion::Data::Model::ApolloEntry.count
|
|
146
|
-
stats[:by_status] = Legion::Data::Model::ApolloEntry.group_and_count(:status).all
|
|
147
|
-
.to_h { |r| [r[:status], r[:count]] }
|
|
148
|
-
stats[:by_content_type] = Legion::Data::Model::ApolloEntry.group_and_count(:content_type).all
|
|
149
|
-
.to_h { |r| [r[:content_type], r[:count]] }
|
|
150
|
-
stats[:total_relations] = Legion::Data::Model::ApolloRelation.count if defined?(Legion::Data::Model::ApolloRelation)
|
|
151
|
-
else
|
|
152
|
-
stats[:error] = 'apollo_data_not_available'
|
|
153
|
-
end
|
|
154
|
-
stats.to_json
|
|
174
|
+
self.class.stats_payload.to_json
|
|
155
175
|
end
|
|
156
176
|
end
|
|
157
177
|
end
|
|
@@ -85,9 +85,8 @@ module Legion
|
|
|
85
85
|
|
|
86
86
|
content = normalize_text_input(content)
|
|
87
87
|
log.debug("Apollo Knowledge.handle_ingest content_length=#{content.length} content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'}") # rubocop:disable Layout/LineLength
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
88
|
+
early_error = ingest_early_return_error(content: content, content_type: content_type, tags: tags)
|
|
89
|
+
return early_error if early_error
|
|
91
90
|
|
|
92
91
|
hash = content_hash || (defined?(Helpers::Writeback) ? Helpers::Writeback.content_hash(content) : nil)
|
|
93
92
|
existing = active_duplicate_for_hash(hash)
|
|
@@ -353,6 +352,27 @@ module Legion
|
|
|
353
352
|
|
|
354
353
|
private
|
|
355
354
|
|
|
355
|
+
def ingest_early_return_error(content:, content_type:, tags:)
|
|
356
|
+
if content.strip.empty?
|
|
357
|
+
safe_tags = Array(tags).map(&:to_s).map { |t| t.gsub(/[\r\n]+/, ' ') }
|
|
358
|
+
log.warn('[apollo][handle_ingest] early-return: content is required ' \
|
|
359
|
+
"content_type=#{content_type} tags=#{safe_tags.inspect}")
|
|
360
|
+
return { success: false, error: 'content is required' }
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if content_type.nil?
|
|
364
|
+
log.warn('[apollo][handle_ingest] early-return: content_type is required ' \
|
|
365
|
+
"content_length=#{content.to_s.length}")
|
|
366
|
+
return { success: false, error: 'content_type is required' }
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
return nil if defined?(Legion::Data::Model::ApolloEntry)
|
|
370
|
+
|
|
371
|
+
log.warn('[apollo][handle_ingest] early-return: apollo_data_not_available ' \
|
|
372
|
+
"content_type=#{content_type}")
|
|
373
|
+
{ success: false, error: 'apollo_data_not_available' }
|
|
374
|
+
end
|
|
375
|
+
|
|
356
376
|
def normalize_content_type(raw)
|
|
357
377
|
sym = raw.to_s.delete_prefix(':').gsub(%r{[/\s]}, '_').strip.downcase.to_sym
|
|
358
378
|
sym = CONTENT_TYPE_ALIASES.fetch(sym, sym)
|
|
@@ -25,4 +25,68 @@ RSpec.describe Legion::Extensions::Apollo::Api do
|
|
|
25
25
|
it 'is defined as a Sinatra app' do
|
|
26
26
|
expect(described_class.superclass).to eq(Sinatra::Base)
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
describe '.stats_payload' do
|
|
30
|
+
let(:entry_model) { class_double('Legion::Data::Model::ApolloEntry') }
|
|
31
|
+
let(:relation_model) { class_double('Legion::Data::Model::ApolloRelation', count: 4) }
|
|
32
|
+
let(:status_counts) do
|
|
33
|
+
instance_double(
|
|
34
|
+
'StatusCounts',
|
|
35
|
+
all: [
|
|
36
|
+
{ status: 'candidate', count: 2 },
|
|
37
|
+
{ status: 'confirmed', count: 3 },
|
|
38
|
+
{ status: 'archived', count: 1 }
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
let(:content_type_counts) do
|
|
43
|
+
instance_double(
|
|
44
|
+
'ContentTypeCounts',
|
|
45
|
+
all: [
|
|
46
|
+
{ content_type: 'document_chunk', count: 5 },
|
|
47
|
+
{ content_type: 'observation', count: 1 }
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
let(:active_entries) { instance_double('ActiveEntries', count: 5) }
|
|
52
|
+
let(:recent_entries) { instance_double('RecentEntries', count: 2) }
|
|
53
|
+
|
|
54
|
+
before do
|
|
55
|
+
stub_const('Legion::Data::Model::ApolloEntry', entry_model)
|
|
56
|
+
stub_const('Legion::Data::Model::ApolloRelation', relation_model)
|
|
57
|
+
allow(entry_model).to receive(:count).and_return(6)
|
|
58
|
+
allow(entry_model).to receive(:avg).with(:confidence).and_return(0.81234)
|
|
59
|
+
allow(entry_model).to receive(:exclude).with(status: 'archived').and_return(active_entries)
|
|
60
|
+
allow(entry_model).to receive(:where).and_return(recent_entries)
|
|
61
|
+
allow(entry_model).to receive(:group_and_count).with(:status).and_return(status_counts)
|
|
62
|
+
allow(entry_model).to receive(:group_and_count).with(:content_type).and_return(content_type_counts)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns the health UI metrics expected by Interlink' do
|
|
66
|
+
payload = described_class.stats_payload(now: Time.utc(2026, 4, 28, 12, 0, 0))
|
|
67
|
+
|
|
68
|
+
expect(payload).to include(
|
|
69
|
+
total_entries: 6,
|
|
70
|
+
recent_24h: 2,
|
|
71
|
+
avg_confidence: 0.812,
|
|
72
|
+
total_relations: 4
|
|
73
|
+
)
|
|
74
|
+
expect(payload[:by_status]).to include(
|
|
75
|
+
'candidate' => 2,
|
|
76
|
+
'confirmed' => 3,
|
|
77
|
+
'archived' => 1,
|
|
78
|
+
'active' => 5
|
|
79
|
+
)
|
|
80
|
+
expect(payload[:by_content_type]).to eq(
|
|
81
|
+
'document_chunk' => 5,
|
|
82
|
+
'observation' => 1
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns an apollo data error when the entry model is unavailable' do
|
|
87
|
+
hide_const('Legion::Data::Model::ApolloEntry')
|
|
88
|
+
|
|
89
|
+
expect(described_class.stats_payload).to eq(error: 'apollo_data_not_available')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
28
92
|
end
|
|
@@ -337,6 +337,36 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
337
337
|
)
|
|
338
338
|
end
|
|
339
339
|
end
|
|
340
|
+
|
|
341
|
+
context 'early-return warn logs' do
|
|
342
|
+
let(:logger) { instance_double('Logger', debug: nil, info: nil, warn: nil) }
|
|
343
|
+
|
|
344
|
+
before { allow(host).to receive(:log).and_return(logger) }
|
|
345
|
+
|
|
346
|
+
it 'emits a warn log when content is nil' do
|
|
347
|
+
host.handle_ingest(content: nil, content_type: 'fact')
|
|
348
|
+
expect(logger).to have_received(:warn).with(/early-return: content is required/)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'emits a warn log when content_type is nil' do
|
|
352
|
+
host.handle_ingest(content: 'something', content_type: nil)
|
|
353
|
+
expect(logger).to have_received(:warn).with(/early-return: content_type is required/)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it 'emits a warn log when apollo_data_not_available' do
|
|
357
|
+
hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
|
|
358
|
+
host.handle_ingest(content: 'something', content_type: 'fact')
|
|
359
|
+
expect(logger).to have_received(:warn).with(/early-return: apollo_data_not_available/)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'sanitizes newline-bearing tags in the warn log' do
|
|
363
|
+
host.handle_ingest(content: nil, content_type: 'fact', tags: ["evil\nFAKE LOG LINE", 'normal'])
|
|
364
|
+
expect(logger).to have_received(:warn) do |msg|
|
|
365
|
+
expect(msg).to include('evil FAKE LOG LINE')
|
|
366
|
+
expect(msg).not_to include("\n")
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
340
370
|
end
|
|
341
371
|
|
|
342
372
|
describe '#handle_query' do
|