legion-apollo 0.5.3 → 0.5.5
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 +16 -0
- data/lib/legion/apollo/local/migrations/006_add_identity_and_access_scope.rb +25 -0
- data/lib/legion/apollo/local.rb +53 -20
- data/lib/legion/apollo/version.rb +1 -1
- data/lib/legion/apollo.rb +32 -9
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da837d4b00044d6028f0d62cfcf972c1da2a15121c182c7e6306e188e6b8c5b8
|
|
4
|
+
data.tar.gz: 7eabbcc8623e98f023f9c1d39c824719336ea83826047ec7afe59d97fafa595f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d5167b38adf9a921a97e72e0304a6b7c2d3b1706c105ffaf48dcbaa8e66af42b21c42e36d6b1f78d2b765fc1796ebffcb3aeed2a224a6343c43bb077aefd9ba
|
|
7
|
+
data.tar.gz: 6219138ff5582bcf5433b37ee3cadc59946df54c4d9cf64bba71e878e8062ed31fe9703bb66ae92f7849ed87eb0d8dba0f9a38c41d109e1f35ee5d127c97c55d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.5] - 2026-05-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Apollo.ingest` and `Legion::Apollo::Local.ingest`/`upsert` now auto-inject `access_scope` (default `'global'`), `identity_canonical_name`, `identity_principal_id`, and `identity_id` from `Legion::Identity::Process` — zero call-site changes required; guarded by `defined?` so nodes without Identity loaded are unaffected
|
|
7
|
+
- Migration 006 adds the four identity/scope columns to `local_knowledge` with `access_scope` defaulting to `'global'` to match the Phase 1 Postgres schema
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `query_merged` now forwards `requesting_principal_id` to `Apollo::Local.query` so the access-scope filter is honoured in merged results
|
|
11
|
+
- `Apollo::Local.query` accepts `requesting_principal_id:` and suppresses `private` entries whose `identity_principal_id` does not match the requesting principal (`nil` principal = system/background caller, all entries visible)
|
|
12
|
+
|
|
13
|
+
## [0.5.4] - 2026-05-07
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Preserve distinct merged query results when local or global Apollo entries arrive without a `content_hash`.
|
|
17
|
+
- Detect co-located Apollo reader and writer runners by module presence so in-process routing is not skipped.
|
|
18
|
+
|
|
3
19
|
## [0.5.3] - 2026-04-27
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
alter_table(:local_knowledge) do
|
|
6
|
+
add_column :access_scope, String, null: false, default: 'global'
|
|
7
|
+
add_column :identity_canonical_name, String, null: true
|
|
8
|
+
add_column :identity_principal_id, Integer, null: true
|
|
9
|
+
add_column :identity_id, Integer, null: true
|
|
10
|
+
|
|
11
|
+
add_index :access_scope, name: :idx_local_knowledge_access_scope
|
|
12
|
+
add_index :identity_principal_id, name: :idx_local_knowledge_identity_principal_id
|
|
13
|
+
add_index :identity_id, name: :idx_local_knowledge_identity_id
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
down do
|
|
18
|
+
alter_table(:local_knowledge) do
|
|
19
|
+
drop_column :access_scope
|
|
20
|
+
drop_column :identity_canonical_name
|
|
21
|
+
drop_column :identity_principal_id
|
|
22
|
+
drop_column :identity_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/legion/apollo/local.rb
CHANGED
|
@@ -46,12 +46,13 @@ module Legion
|
|
|
46
46
|
@started == true
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def ingest(content:, tags: [], **opts) # rubocop:disable Metrics/MethodLength
|
|
49
|
+
def ingest(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength
|
|
50
50
|
return not_started_error unless started?
|
|
51
51
|
|
|
52
52
|
tags = normalize_tags_input(tags)
|
|
53
53
|
WRITE_MUTEX.synchronize do
|
|
54
|
-
ingest_without_lock(content: content, tags: tags,
|
|
54
|
+
ingest_without_lock(content: content, tags: tags,
|
|
55
|
+
**inject_identity_context(opts).merge(access_scope: access_scope))
|
|
55
56
|
end
|
|
56
57
|
rescue StandardError => e
|
|
57
58
|
handle_exception(
|
|
@@ -64,18 +65,19 @@ module Legion
|
|
|
64
65
|
{ success: false, error: e.message }
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
def upsert(content:, tags: [], **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
68
|
+
def upsert(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
68
69
|
return not_started_error unless started?
|
|
69
70
|
|
|
70
71
|
sorted_tags = normalize_tags_input(tags).sort
|
|
71
72
|
tag_json = Legion::JSON.dump(sorted_tags)
|
|
73
|
+
merged_opts = inject_identity_context(opts).merge(access_scope: access_scope)
|
|
72
74
|
WRITE_MUTEX.synchronize do
|
|
73
75
|
existing = db[:local_knowledge].where(tags: tag_json).first
|
|
74
76
|
|
|
75
77
|
if existing
|
|
76
|
-
update_upsert_entry(existing, content, tag_json,
|
|
78
|
+
update_upsert_entry(existing, content, tag_json, merged_opts)
|
|
77
79
|
else
|
|
78
|
-
result = ingest_without_lock(content: content, tags: sorted_tags, **
|
|
80
|
+
result = ingest_without_lock(content: content, tags: sorted_tags, **merged_opts)
|
|
79
81
|
result[:mode] = :inserted if result[:success] && result[:mode] != :deduplicated
|
|
80
82
|
result
|
|
81
83
|
end
|
|
@@ -91,7 +93,7 @@ module Legion
|
|
|
91
93
|
{ success: false, error: e.message }
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
def query(text:, limit: nil, min_confidence: nil, tags: nil, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
96
|
+
def query(text:, limit: nil, min_confidence: nil, tags: nil, requesting_principal_id: nil, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/ParameterLists
|
|
95
97
|
return not_started_error unless started?
|
|
96
98
|
|
|
97
99
|
text = normalize_text_input(text)
|
|
@@ -111,7 +113,8 @@ module Legion
|
|
|
111
113
|
include_history = opts.fetch(:include_history, false)
|
|
112
114
|
candidates = filter_candidates(candidates, min_confidence: min_confidence, tags: tags,
|
|
113
115
|
options: { include_inferences: include_inferences,
|
|
114
|
-
include_history: include_history, as_of: as_of
|
|
116
|
+
include_history: include_history, as_of: as_of,
|
|
117
|
+
requesting_principal_id: requesting_principal_id })
|
|
115
118
|
candidates = cosine_rerank(text, candidates) if can_rerank?
|
|
116
119
|
results = candidates.first(limit)
|
|
117
120
|
|
|
@@ -496,8 +499,13 @@ module Legion
|
|
|
496
499
|
end
|
|
497
500
|
|
|
498
501
|
def ingest_source_columns(opts)
|
|
499
|
-
{ source_channel:
|
|
500
|
-
|
|
502
|
+
{ source_channel: opts[:source_channel],
|
|
503
|
+
source_agent: opts[:source_agent],
|
|
504
|
+
submitted_by: opts[:submitted_by],
|
|
505
|
+
access_scope: opts[:access_scope] || 'global',
|
|
506
|
+
identity_canonical_name: opts[:identity_canonical_name],
|
|
507
|
+
identity_principal_id: opts[:identity_principal_id],
|
|
508
|
+
identity_id: opts[:identity_id] }
|
|
501
509
|
end
|
|
502
510
|
|
|
503
511
|
def ingest_lineage_columns(opts)
|
|
@@ -653,6 +661,13 @@ module Legion
|
|
|
653
661
|
tag_set.intersect?(entry_tags)
|
|
654
662
|
end
|
|
655
663
|
end
|
|
664
|
+
pid = options[:requesting_principal_id]
|
|
665
|
+
if pid
|
|
666
|
+
candidates = candidates.select do |c|
|
|
667
|
+
scope = c[:access_scope] || 'global'
|
|
668
|
+
scope != 'private' || c[:identity_principal_id] == pid
|
|
669
|
+
end
|
|
670
|
+
end
|
|
656
671
|
candidates
|
|
657
672
|
end
|
|
658
673
|
|
|
@@ -841,17 +856,21 @@ module Legion
|
|
|
841
856
|
|
|
842
857
|
db.transaction do
|
|
843
858
|
db[:local_knowledge].where(id: existing[:id]).update(
|
|
844
|
-
content:
|
|
845
|
-
content_hash:
|
|
846
|
-
tags:
|
|
847
|
-
embedding:
|
|
848
|
-
embedded_at:
|
|
849
|
-
confidence:
|
|
850
|
-
expires_at:
|
|
851
|
-
source_channel:
|
|
852
|
-
source_agent:
|
|
853
|
-
submitted_by:
|
|
854
|
-
|
|
859
|
+
content: content,
|
|
860
|
+
content_hash: new_hash,
|
|
861
|
+
tags: tags_json,
|
|
862
|
+
embedding: embedding ? Legion::JSON.dump(embedding) : nil,
|
|
863
|
+
embedded_at: embedded_at,
|
|
864
|
+
confidence: opts.fetch(:confidence, existing[:confidence]),
|
|
865
|
+
expires_at: expires_at,
|
|
866
|
+
source_channel: opts.fetch(:source_channel, existing[:source_channel]),
|
|
867
|
+
source_agent: opts.fetch(:source_agent, existing[:source_agent]),
|
|
868
|
+
submitted_by: opts.fetch(:submitted_by, existing[:submitted_by]),
|
|
869
|
+
access_scope: opts.fetch(:access_scope, existing[:access_scope]) || 'global',
|
|
870
|
+
identity_canonical_name: opts.fetch(:identity_canonical_name, existing[:identity_canonical_name]),
|
|
871
|
+
identity_principal_id: opts.fetch(:identity_principal_id, existing[:identity_principal_id]),
|
|
872
|
+
identity_id: opts.fetch(:identity_id, existing[:identity_id]),
|
|
873
|
+
updated_at: now
|
|
855
874
|
)
|
|
856
875
|
rebuild_fts_entry!(existing[:id], content, tags_json)
|
|
857
876
|
end
|
|
@@ -880,6 +899,20 @@ module Legion
|
|
|
880
899
|
end
|
|
881
900
|
end
|
|
882
901
|
|
|
902
|
+
def inject_identity_context(opts)
|
|
903
|
+
return opts unless defined?(Legion::Identity::Process)
|
|
904
|
+
|
|
905
|
+
id = Legion::Identity::Process.identity_hash
|
|
906
|
+
{
|
|
907
|
+
identity_canonical_name: id[:canonical_name],
|
|
908
|
+
identity_principal_id: id[:db_principal_id],
|
|
909
|
+
identity_id: id[:db_identity_id]
|
|
910
|
+
}.compact.merge(opts)
|
|
911
|
+
rescue StandardError => e
|
|
912
|
+
handle_exception(e, level: :warn, operation: 'apollo.local.inject_identity_context')
|
|
913
|
+
opts
|
|
914
|
+
end
|
|
915
|
+
|
|
883
916
|
def not_started_error
|
|
884
917
|
{ success: false, error: :not_started }
|
|
885
918
|
end
|
data/lib/legion/apollo.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
|
+
require 'legion/json'
|
|
4
5
|
require 'legion/logging'
|
|
6
|
+
require 'legion/settings'
|
|
5
7
|
require_relative 'apollo/version'
|
|
6
8
|
require_relative 'apollo/settings'
|
|
7
9
|
require_relative 'apollo/helpers/tag_normalizer'
|
|
@@ -93,13 +95,14 @@ module Legion
|
|
|
93
95
|
end
|
|
94
96
|
end
|
|
95
97
|
|
|
96
|
-
def ingest(content:, tags: [], scope: :global, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
98
|
+
def ingest(content:, tags: [], scope: :global, access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
97
99
|
return not_started_error unless started?
|
|
98
100
|
|
|
99
101
|
normalized_tags = normalize_tags_input(tags)
|
|
100
102
|
normalized_content = normalize_text_input(content)
|
|
101
103
|
normalized_raw_content = normalize_raw_content_input(opts[:raw_content], fallback: normalized_content)
|
|
102
|
-
payload = { **opts, content: normalized_content,
|
|
104
|
+
payload = { **inject_identity_context(opts), content: normalized_content,
|
|
105
|
+
raw_content: normalized_raw_content, tags: normalized_tags, access_scope: access_scope }
|
|
103
106
|
log.info do
|
|
104
107
|
"Apollo ingest requested scope=#{scope} content_length=#{payload[:content].to_s.length} " \
|
|
105
108
|
"tags=#{payload[:tags].size} source_channel=#{payload[:source_channel]}"
|
|
@@ -224,8 +227,7 @@ module Legion
|
|
|
224
227
|
def co_located_reader?
|
|
225
228
|
return false unless data_available?
|
|
226
229
|
|
|
227
|
-
defined?(Legion::Extensions::Apollo::Runners::Knowledge)
|
|
228
|
-
Legion::Extensions::Apollo::Runners::Knowledge.respond_to?(:handle_query)
|
|
230
|
+
defined?(Legion::Extensions::Apollo::Runners::Knowledge) ? true : false
|
|
229
231
|
rescue StandardError => e
|
|
230
232
|
handle_exception(e, level: :debug, operation: 'apollo.co_located_reader')
|
|
231
233
|
false
|
|
@@ -234,8 +236,7 @@ module Legion
|
|
|
234
236
|
def co_located_writer?
|
|
235
237
|
return false unless data_available?
|
|
236
238
|
|
|
237
|
-
defined?(Legion::Extensions::Apollo::Runners::Knowledge)
|
|
238
|
-
Legion::Extensions::Apollo::Runners::Knowledge.respond_to?(:handle_ingest)
|
|
239
|
+
defined?(Legion::Extensions::Apollo::Runners::Knowledge) ? true : false
|
|
239
240
|
rescue StandardError => e
|
|
240
241
|
handle_exception(e, level: :debug, operation: 'apollo.co_located_writer')
|
|
241
242
|
false
|
|
@@ -292,7 +293,8 @@ module Legion
|
|
|
292
293
|
end
|
|
293
294
|
result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
|
|
294
295
|
:tier, :include_inferences, :include_history,
|
|
295
|
-
:as_of)
|
|
296
|
+
:as_of),
|
|
297
|
+
requesting_principal_id: payload[:requesting_principal_id])
|
|
296
298
|
return result unless result[:success]
|
|
297
299
|
|
|
298
300
|
entries = normalize_local_entries(Array(result[:results]))
|
|
@@ -328,7 +330,8 @@ module Legion
|
|
|
328
330
|
attempted = true
|
|
329
331
|
local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
|
|
330
332
|
:tier, :include_inferences, :include_history,
|
|
331
|
-
:as_of)
|
|
333
|
+
:as_of),
|
|
334
|
+
requesting_principal_id: payload[:requesting_principal_id])
|
|
332
335
|
if local[:success]
|
|
333
336
|
any_success = true
|
|
334
337
|
entries.concat(normalize_local_entries(Array(local[:results]))) if local[:results]
|
|
@@ -407,7 +410,13 @@ module Legion
|
|
|
407
410
|
end
|
|
408
411
|
|
|
409
412
|
def dedup_and_rank(entries, limit:)
|
|
410
|
-
|
|
413
|
+
entries_with_hashes = entries.map do |e|
|
|
414
|
+
next e if e[:content_hash]
|
|
415
|
+
|
|
416
|
+
e.merge(content_hash: Digest::MD5.hexdigest(e[:content].to_s.strip.downcase.gsub(/\s+/, ' ')))
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
sorted = entries_with_hashes
|
|
411
420
|
.sort_by { |e| -(e[:confidence] || 0) }
|
|
412
421
|
.uniq { |e| e[:content_hash] }
|
|
413
422
|
|
|
@@ -554,6 +563,20 @@ module Legion
|
|
|
554
563
|
end
|
|
555
564
|
end
|
|
556
565
|
|
|
566
|
+
def inject_identity_context(opts)
|
|
567
|
+
return opts unless defined?(Legion::Identity::Process)
|
|
568
|
+
|
|
569
|
+
id = Legion::Identity::Process.identity_hash
|
|
570
|
+
{
|
|
571
|
+
identity_canonical_name: id[:canonical_name],
|
|
572
|
+
identity_principal_id: id[:db_principal_id],
|
|
573
|
+
identity_id: id[:db_identity_id]
|
|
574
|
+
}.compact.merge(opts)
|
|
575
|
+
rescue StandardError => e
|
|
576
|
+
handle_exception(e, level: :warn, operation: 'apollo.inject_identity_context')
|
|
577
|
+
opts
|
|
578
|
+
end
|
|
579
|
+
|
|
557
580
|
def not_started_error
|
|
558
581
|
{ success: false, error: :not_started }
|
|
559
582
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-apollo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -88,6 +88,7 @@ files:
|
|
|
88
88
|
- lib/legion/apollo/local/migrations/003_harden_graph_relationships.rb
|
|
89
89
|
- lib/legion/apollo/local/migrations/004_add_versioning_tiers_inference.rb
|
|
90
90
|
- lib/legion/apollo/local/migrations/005_add_raw_content_temporal_windows.rb
|
|
91
|
+
- lib/legion/apollo/local/migrations/006_add_identity_and_access_scope.rb
|
|
91
92
|
- lib/legion/apollo/messages/access_boost.rb
|
|
92
93
|
- lib/legion/apollo/messages/ingest.rb
|
|
93
94
|
- lib/legion/apollo/messages/query.rb
|