lex-apollo 0.4.27 → 0.4.30
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 +15 -0
- data/lib/legion/extensions/apollo/actors/contradiction_scanner.rb +60 -0
- data/lib/legion/extensions/apollo/actors/entity_watchdog.rb +1 -1
- data/lib/legion/extensions/apollo/helpers/confidence.rb +10 -17
- data/lib/legion/extensions/apollo/helpers/entity_watchdog.rb +1 -1
- data/lib/legion/extensions/apollo/helpers/graph_query.rb +7 -5
- data/lib/legion/extensions/apollo/runners/entity_extractor.rb +1 -1
- data/lib/legion/extensions/apollo/runners/expertise.rb +3 -3
- data/lib/legion/extensions/apollo/runners/gas.rb +5 -5
- data/lib/legion/extensions/apollo/runners/knowledge.rb +59 -18
- data/lib/legion/extensions/apollo/runners/maintenance.rb +3 -3
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/lib/legion/extensions/apollo.rb +2 -0
- data/spec/legion/extensions/apollo/actors/entity_watchdog_spec.rb +1 -1
- data/spec/legion/extensions/apollo/helpers/writeback_spec.rb +2 -2
- data/spec/legion/extensions/apollo/runners/entity_extractor_spec.rb +2 -2
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +95 -11
- 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: facad4c6e5cf4da1497d5d6de01daa13310bc8762274e227e9ff7ead72c761a6
|
|
4
|
+
data.tar.gz: 37a7098b002770c827173135a5510eeb7651f0705ef6f80ded0f8433a012bee1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f45fbaf1de91fd1676d94063f6b8d34663207d0f81de84805eca6203789f6864fe48f54137a0cd73178fc7972f309dec9a3ef8bc956070ec09fd5bbac74c4e9
|
|
7
|
+
data.tar.gz: ff8f92d7ffe8f69d040d00237227c14696453abe76b3bdd90163ae2ffdc681bd0059ca119c6216a2f454f862fdd4db80196c2befa8c1e07b0c73a430fee5a06e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.30] - 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
- **`apollo_setting` wrapper method deleted.** All 40 call sites across 10 files converted to direct bracket access against registered defaults in `default_settings`. Single source of truth for all Apollo configuration per standards section 3.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **Contradiction detection moved off the synchronous write path.** `handle_ingest` no longer blocks on `detect_contradictions`. When enabled, contradiction work is enqueued to the new `ContradictionScanner` interval actor (10-minute cadence, configurable via `apollo.actors.contradiction_interval`). The return payload retains a stable `contradictions: []` key.
|
|
10
|
+
- New setting `apollo.contradiction.enabled` (default: `false`) controls whether contradiction detection runs at all. When false, no LLM calls or vector scans are performed for contradiction purposes.
|
|
11
|
+
- Corrected CLAUDE.md backing store description from Azure to self-hosted PostgreSQL + pgvector.
|
|
12
|
+
- All helpers, runners, and actors now read settings via bracket access (`settings[:namespace][:key]`) instead of the removed `apollo_setting(*keys, default:)` proxy.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `Actor::ContradictionScanner` — interval actor that drains a thread-safe queue of pending entries and runs `detect_contradictions` out-of-band from the write path.
|
|
16
|
+
- `contradiction_interval` setting under `apollo.actors` (default: 600 seconds).
|
|
17
|
+
|
|
3
18
|
## [0.4.27] - 2026-05-15
|
|
4
19
|
|
|
5
20
|
### Added
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
require_relative '../runners/knowledge'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Apollo
|
|
9
|
+
module Actor
|
|
10
|
+
class ContradictionScanner < Legion::Extensions::Actors::Every
|
|
11
|
+
include Legion::Settings::Helper
|
|
12
|
+
include Legion::Logging::Helper
|
|
13
|
+
|
|
14
|
+
@queue = Queue.new
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
attr_reader :queue, :mutex
|
|
19
|
+
|
|
20
|
+
def enqueue(entry_id:, embedding:, content:)
|
|
21
|
+
queue.push({ entry_id: entry_id, embedding: embedding, content: content })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pending_count
|
|
25
|
+
queue.size
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def drain
|
|
29
|
+
items = []
|
|
30
|
+
items << queue.pop until queue.empty?
|
|
31
|
+
items
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def runner_class = Legion::Extensions::Apollo::Runners::Knowledge
|
|
36
|
+
def runner_function = 'scan_pending_contradictions'
|
|
37
|
+
def time = settings[:actors][:contradiction_interval]
|
|
38
|
+
def run_now? = false
|
|
39
|
+
def use_runner? = false
|
|
40
|
+
def check_subtask? = false
|
|
41
|
+
def generate_task? = false
|
|
42
|
+
|
|
43
|
+
def every
|
|
44
|
+
items = self.class.drain
|
|
45
|
+
return if items.empty?
|
|
46
|
+
|
|
47
|
+
log.debug("Apollo ContradictionScanner.every processing=#{items.size}")
|
|
48
|
+
runner = Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
|
|
49
|
+
items.each do |item|
|
|
50
|
+
runner.detect_contradictions(item[:entry_id], item[:embedding], item[:content])
|
|
51
|
+
end
|
|
52
|
+
log.info("Apollo ContradictionScanner.every completed=#{items.size}")
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
handle_exception(e, level: :error, operation: 'apollo.actor.contradiction_scanner')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -89,7 +89,7 @@ module Legion
|
|
|
89
89
|
result = retrieve_relevant(
|
|
90
90
|
query: entity[:name].to_s,
|
|
91
91
|
limit: 1,
|
|
92
|
-
min_confidence:
|
|
92
|
+
min_confidence: settings[:entity_watchdog][:exists_min_confidence],
|
|
93
93
|
tags: [entity[:type].to_s]
|
|
94
94
|
)
|
|
95
95
|
return false unless result[:success] && result[:count].positive?
|
|
@@ -24,25 +24,18 @@ module Legion
|
|
|
24
24
|
|
|
25
25
|
module_function
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def
|
|
35
|
-
def
|
|
36
|
-
def retrieval_boost = apollo_setting(:confidence, :retrieval_boost, default: RETRIEVAL_BOOST)
|
|
37
|
-
def power_law_alpha = apollo_setting(:power_law_alpha, default: POWER_LAW_ALPHA)
|
|
38
|
-
def decay_threshold = apollo_setting(:decay_threshold, default: DECAY_THRESHOLD)
|
|
39
|
-
def write_confidence_gate = apollo_setting(:confidence, :write_gate, default: WRITE_CONFIDENCE_GATE)
|
|
40
|
-
def write_novelty_gate = apollo_setting(:confidence, :novelty_gate, default: WRITE_NOVELTY_GATE)
|
|
41
|
-
def stale_days = apollo_setting(:stale_days, default: STALE_DAYS)
|
|
42
|
-
def decay_min_age_hours = apollo_setting(:decay_min_age_hours, default: DECAY_MIN_AGE_HOURS)
|
|
27
|
+
def initial_confidence = settings[:confidence][:initial]
|
|
28
|
+
def corroboration_boost = settings[:confidence][:corroboration_boost]
|
|
29
|
+
def retrieval_boost = settings[:confidence][:retrieval_boost]
|
|
30
|
+
def power_law_alpha = settings[:power_law_alpha]
|
|
31
|
+
def decay_threshold = settings[:decay_threshold]
|
|
32
|
+
def write_confidence_gate = settings[:confidence][:write_gate]
|
|
33
|
+
def write_novelty_gate = settings[:confidence][:novelty_gate]
|
|
34
|
+
def stale_days = settings[:stale_days]
|
|
35
|
+
def decay_min_age_hours = settings[:decay_min_age_hours]
|
|
43
36
|
|
|
44
37
|
def corroboration_similarity_threshold
|
|
45
|
-
|
|
38
|
+
settings[:confidence][:corroboration_similarity]
|
|
46
39
|
end
|
|
47
40
|
|
|
48
41
|
def apply_decay(confidence:, age_hours: nil, alpha: power_law_alpha, **)
|
|
@@ -28,7 +28,7 @@ module Legion
|
|
|
28
28
|
|
|
29
29
|
text.scan(pattern).each do |match|
|
|
30
30
|
entities << { type: entity_type, value: match.strip,
|
|
31
|
-
confidence:
|
|
31
|
+
confidence: settings[:entity_watchdog][:detect_confidence] }
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -5,18 +5,20 @@ module Legion
|
|
|
5
5
|
module Apollo
|
|
6
6
|
module Helpers
|
|
7
7
|
module GraphQuery
|
|
8
|
+
extend Legion::Settings::Helper
|
|
9
|
+
|
|
8
10
|
SPREAD_FACTOR = 0.6
|
|
9
11
|
DEFAULT_DEPTH = 2
|
|
10
12
|
MIN_ACTIVATION = 0.1
|
|
11
13
|
|
|
12
14
|
module_function
|
|
13
15
|
|
|
14
|
-
def spread_factor =
|
|
15
|
-
def default_depth =
|
|
16
|
-
def min_activation =
|
|
16
|
+
def spread_factor = settings[:graph][:spread_factor]
|
|
17
|
+
def default_depth = settings[:graph][:default_depth]
|
|
18
|
+
def min_activation = settings[:graph][:min_activation]
|
|
17
19
|
|
|
18
|
-
def default_query_limit =
|
|
19
|
-
def default_query_min_confidence =
|
|
20
|
+
def default_query_limit = settings[:query][:default_limit]
|
|
21
|
+
def default_query_min_confidence = settings[:query][:default_min_confidence]
|
|
20
22
|
|
|
21
23
|
def build_traversal_sql(depth: default_depth, relation_types: nil, min_activation: self.min_activation, **)
|
|
22
24
|
sf = spread_factor
|
|
@@ -8,7 +8,7 @@ module Legion
|
|
|
8
8
|
DEFAULT_ENTITY_TYPES = %w[person service repository concept].freeze
|
|
9
9
|
DEFAULT_MIN_CONFIDENCE = 0.7
|
|
10
10
|
|
|
11
|
-
def extract_entities(text:, entity_types: nil, min_confidence:
|
|
11
|
+
def extract_entities(text:, entity_types: nil, min_confidence: Legion::Extensions::Apollo.settings[:entity_extractor][:min_confidence], **)
|
|
12
12
|
if text.to_s.strip.empty?
|
|
13
13
|
log.debug('Apollo EntityExtractor.extract_entities skipped reason=empty_text')
|
|
14
14
|
return { success: true, entities: [], source: :empty }
|
|
@@ -7,11 +7,11 @@ module Legion
|
|
|
7
7
|
module Apollo
|
|
8
8
|
module Runners
|
|
9
9
|
module Expertise
|
|
10
|
-
def get_expertise(domain:, min_proficiency:
|
|
10
|
+
def get_expertise(domain:, min_proficiency: Legion::Extensions::Apollo.settings[:expertise][:initial_proficiency], **)
|
|
11
11
|
{ action: :expertise_query, domain: domain, min_proficiency: min_proficiency }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def domains_at_risk(min_agents:
|
|
14
|
+
def domains_at_risk(min_agents: Legion::Extensions::Apollo.settings[:expertise][:min_agents_at_risk], **)
|
|
15
15
|
{ action: :domains_at_risk, min_agents: min_agents }
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -75,7 +75,7 @@ module Legion
|
|
|
75
75
|
|
|
76
76
|
def expertise_proficiency(confidences)
|
|
77
77
|
avg = confidences.sum / confidences.size
|
|
78
|
-
cap =
|
|
78
|
+
cap = Legion::Extensions::Apollo.settings[:expertise][:proficiency_cap]
|
|
79
79
|
[avg * Math.log2(confidences.size + 1), cap].min
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -27,11 +27,11 @@ module Legion
|
|
|
27
27
|
json_parse(str)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def relate_confidence_gate =
|
|
31
|
-
def synthesis_confidence_cap =
|
|
32
|
-
def max_anticipations =
|
|
33
|
-
def similar_entries_limit =
|
|
34
|
-
def fallback_confidence =
|
|
30
|
+
def relate_confidence_gate = Legion::Extensions::Apollo.settings[:gas][:relate_confidence_gate]
|
|
31
|
+
def synthesis_confidence_cap = Legion::Extensions::Apollo.settings[:gas][:synthesis_confidence_cap]
|
|
32
|
+
def max_anticipations = Legion::Extensions::Apollo.settings[:gas][:max_anticipations]
|
|
33
|
+
def similar_entries_limit = Legion::Extensions::Apollo.settings[:gas][:similar_entries_limit]
|
|
34
|
+
def fallback_confidence = Legion::Extensions::Apollo.settings[:gas][:fallback_confidence]
|
|
35
35
|
|
|
36
36
|
def process(audit_event)
|
|
37
37
|
unless processable?(audit_event)
|
|
@@ -85,10 +85,10 @@ module Legion
|
|
|
85
85
|
def handle_ingest(content: nil, content_type: nil, tags: [], source_agent: 'unknown', # rubocop:disable Metrics/ParameterLists
|
|
86
86
|
source_provider: nil, source_channel: nil, knowledge_domain: nil,
|
|
87
87
|
submitted_by: nil, submitted_from: nil, content_hash: nil, context: {},
|
|
88
|
-
skip: false, access_scope: 'global',
|
|
89
|
-
identity_id: nil, identity_canonical_name: nil, **)
|
|
88
|
+
skip: false, access_scope: 'global', **)
|
|
90
89
|
return { status: :skipped } if skip
|
|
91
90
|
|
|
91
|
+
identity = resolve_process_identity
|
|
92
92
|
content = normalize_text_input(content)
|
|
93
93
|
content_type = normalize_content_type(content_type.nil? ? :observation : content_type)
|
|
94
94
|
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
|
|
@@ -97,7 +97,7 @@ module Legion
|
|
|
97
97
|
|
|
98
98
|
hash = content_hash || (defined?(Helpers::Writeback) ? Helpers::Writeback.content_hash(content) : nil)
|
|
99
99
|
existing = active_duplicate_for_hash(hash, access_scope: access_scope,
|
|
100
|
-
identity_principal_id:
|
|
100
|
+
identity_principal_id: identity[:principal_id])
|
|
101
101
|
if existing
|
|
102
102
|
log.info("Apollo Knowledge.handle_ingest deduped entry_id=#{existing.id} source_agent=#{source_agent}")
|
|
103
103
|
return { success: true, entry_id: existing.id, deduped: true }
|
|
@@ -109,9 +109,9 @@ module Legion
|
|
|
109
109
|
source_provider: source_provider, source_channel: source_channel,
|
|
110
110
|
submitted_by: submitted_by, submitted_from: submitted_from,
|
|
111
111
|
access_scope: access_scope,
|
|
112
|
-
identity_principal_id:
|
|
113
|
-
identity_id: identity_id,
|
|
114
|
-
identity_canonical_name:
|
|
112
|
+
identity_principal_id: identity[:principal_id],
|
|
113
|
+
identity_id: identity[:identity_id],
|
|
114
|
+
identity_canonical_name: identity[:canonical_name])
|
|
115
115
|
|
|
116
116
|
corroborated, existing_id = find_corroboration(
|
|
117
117
|
embedding, content_type_sym, metadata[:source_agent], metadata[:source_channel]
|
|
@@ -132,11 +132,11 @@ module Legion
|
|
|
132
132
|
entry_id: existing_id, agent_id: metadata[:source_agent], action: 'ingest'
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
log.debug("Apollo Knowledge.handle_ingest complete entry_id=#{existing_id} corroborated=#{corroborated}
|
|
135
|
+
schedule_contradiction_detection(entry_id: existing_id, embedding: embedding, content: content)
|
|
136
|
+
log.debug("Apollo Knowledge.handle_ingest complete entry_id=#{existing_id} corroborated=#{corroborated}")
|
|
137
137
|
|
|
138
138
|
{ success: true, entry_id: existing_id, status: corroborated ? 'corroborated' : 'candidate',
|
|
139
|
-
corroborated: corroborated, contradictions:
|
|
139
|
+
corroborated: corroborated, contradictions: [] }
|
|
140
140
|
rescue Sequel::Error => e
|
|
141
141
|
handle_exception(e, level: :error, operation: 'apollo.knowledge.handle_ingest')
|
|
142
142
|
{ success: false, error: e.message }
|
|
@@ -148,6 +148,7 @@ module Legion
|
|
|
148
148
|
requesting_principal_id: nil, **)
|
|
149
149
|
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
150
150
|
|
|
151
|
+
requesting_principal_id = resolve_requesting_principal_id(requesting_principal_id)
|
|
151
152
|
entry_model = Helpers::DataModels.apollo_entry
|
|
152
153
|
query = normalize_text_input(query)
|
|
153
154
|
status_defaulted = status.equal?(UNSET)
|
|
@@ -231,7 +232,7 @@ module Legion
|
|
|
231
232
|
{ success: false, error: e.message }
|
|
232
233
|
end
|
|
233
234
|
|
|
234
|
-
def redistribute_knowledge(agent_id:, min_confidence:
|
|
235
|
+
def redistribute_knowledge(agent_id:, min_confidence: settings[:query][:redistribute_min_confidence], **)
|
|
235
236
|
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
236
237
|
|
|
237
238
|
log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
|
|
@@ -266,13 +267,14 @@ module Legion
|
|
|
266
267
|
{ success: false, error: e.message }
|
|
267
268
|
end
|
|
268
269
|
|
|
269
|
-
def retrieve_relevant(query: nil, limit:
|
|
270
|
+
def retrieve_relevant(query: nil, limit: settings[:query][:retrieval_limit],
|
|
270
271
|
min_confidence: Helpers::GraphQuery.default_query_min_confidence,
|
|
271
272
|
tags: nil, domain: nil, skip: false, requesting_principal_id: nil, **)
|
|
272
273
|
return { status: :skipped } if skip
|
|
273
274
|
|
|
274
275
|
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
275
276
|
|
|
277
|
+
requesting_principal_id = resolve_requesting_principal_id(requesting_principal_id)
|
|
276
278
|
query = normalize_text_input(query)
|
|
277
279
|
log.debug("Apollo Knowledge.retrieve_relevant query_length=#{query.length} limit=#{limit} min_confidence=#{min_confidence} tags=#{Array(tags).size} domain=#{domain || 'nil'}") # rubocop:disable Layout/LineLength
|
|
278
280
|
return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
|
|
@@ -314,7 +316,7 @@ module Legion
|
|
|
314
316
|
{ success: false, error: e.message }
|
|
315
317
|
end
|
|
316
318
|
|
|
317
|
-
def prepare_mesh_export(target_domain:, min_confidence:
|
|
319
|
+
def prepare_mesh_export(target_domain:, min_confidence: settings[:query][:mesh_export_min_confidence], limit: settings[:query][:mesh_export_limit], **) # rubocop:disable Layout/LineLength
|
|
318
320
|
unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
|
|
319
321
|
return { success: false, error: 'apollo_data_not_available' }
|
|
320
322
|
end
|
|
@@ -594,12 +596,28 @@ module Legion
|
|
|
594
596
|
Array(allowed)
|
|
595
597
|
end
|
|
596
598
|
|
|
599
|
+
def contradiction_detection_enabled?
|
|
600
|
+
settings[:contradiction][:enabled] == true
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def schedule_contradiction_detection(entry_id:, embedding:, content:)
|
|
604
|
+
return unless contradiction_detection_enabled?
|
|
605
|
+
return unless embedding && Helpers::DataModels.apollo_entry_available?
|
|
606
|
+
|
|
607
|
+
Legion::Extensions::Apollo::Actor::ContradictionScanner.enqueue(
|
|
608
|
+
entry_id: entry_id, embedding: embedding, content: content
|
|
609
|
+
)
|
|
610
|
+
log.debug("Apollo Knowledge.schedule_contradiction_detection enqueued entry_id=#{entry_id}")
|
|
611
|
+
rescue StandardError => e
|
|
612
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.schedule_contradiction_detection')
|
|
613
|
+
end
|
|
614
|
+
|
|
597
615
|
def detect_contradictions(entry_id, embedding, content)
|
|
598
616
|
return [] unless embedding && Helpers::DataModels.apollo_entry_available?
|
|
599
617
|
|
|
600
|
-
sim_limit =
|
|
601
|
-
sim_threshold =
|
|
602
|
-
rel_weight =
|
|
618
|
+
sim_limit = settings[:contradiction][:similar_limit]
|
|
619
|
+
sim_threshold = settings[:contradiction][:similarity_threshold]
|
|
620
|
+
rel_weight = settings[:contradiction][:relation_weight]
|
|
603
621
|
|
|
604
622
|
db = Helpers::DataModels.apollo_entry.db
|
|
605
623
|
log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
|
|
@@ -656,7 +674,7 @@ module Legion
|
|
|
656
674
|
def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
|
|
657
675
|
return [false, nil] unless embedding
|
|
658
676
|
|
|
659
|
-
scan_limit =
|
|
677
|
+
scan_limit = settings[:corroboration][:scan_limit]
|
|
660
678
|
log.debug("Apollo Knowledge.find_corroboration content_type=#{content_type_sym} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'} scan_limit=#{scan_limit}") # rubocop:disable Layout/LineLength
|
|
661
679
|
existing = Helpers::DataModels.apollo_entry
|
|
662
680
|
.where(content_type: content_type_sym)
|
|
@@ -669,7 +687,7 @@ module Legion
|
|
|
669
687
|
sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: entry.embedding)
|
|
670
688
|
next unless Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
|
|
671
689
|
|
|
672
|
-
same_provider_wt =
|
|
690
|
+
same_provider_wt = settings[:corroboration][:same_provider_weight]
|
|
673
691
|
weight = same_source_provider?(source_agent, entry) ? same_provider_wt : 1.0
|
|
674
692
|
|
|
675
693
|
# Reject corroboration entirely if same channel (same data source)
|
|
@@ -714,12 +732,35 @@ module Legion
|
|
|
714
732
|
else
|
|
715
733
|
Helpers::DataModels.apollo_expertise.create(
|
|
716
734
|
agent_id: source_agent, domain: domain,
|
|
717
|
-
proficiency:
|
|
735
|
+
proficiency: settings[:expertise][:initial_proficiency],
|
|
718
736
|
entry_count: 1, last_active_at: Time.now
|
|
719
737
|
)
|
|
720
738
|
end
|
|
721
739
|
end
|
|
722
740
|
|
|
741
|
+
def resolve_process_identity
|
|
742
|
+
return { principal_id: nil, identity_id: nil, canonical_name: nil } unless defined?(Legion::Identity::Process)
|
|
743
|
+
|
|
744
|
+
{
|
|
745
|
+
principal_id: Legion::Identity::Process.db_principal_id,
|
|
746
|
+
identity_id: Legion::Identity::Process.db_identity_id,
|
|
747
|
+
canonical_name: Legion::Identity::Process.canonical_name
|
|
748
|
+
}
|
|
749
|
+
rescue StandardError => e
|
|
750
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.resolve_process_identity')
|
|
751
|
+
{ principal_id: nil, identity_id: nil, canonical_name: nil }
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def resolve_requesting_principal_id(explicit_value)
|
|
755
|
+
return explicit_value if explicit_value
|
|
756
|
+
return nil unless defined?(Legion::Identity::Process)
|
|
757
|
+
|
|
758
|
+
Legion::Identity::Process.db_principal_id
|
|
759
|
+
rescue StandardError => e
|
|
760
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.resolve_requesting_principal_id')
|
|
761
|
+
nil
|
|
762
|
+
end
|
|
763
|
+
|
|
723
764
|
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
724
765
|
include Legion::JSON::Helper
|
|
725
766
|
include Legion::Settings::Helper
|
|
@@ -9,7 +9,7 @@ module Legion
|
|
|
9
9
|
module Apollo
|
|
10
10
|
module Runners
|
|
11
11
|
module Maintenance
|
|
12
|
-
def force_decay(factor:
|
|
12
|
+
def force_decay(factor: Legion::Extensions::Apollo.settings[:maintenance][:force_decay_factor], **)
|
|
13
13
|
{ action: :force_decay, factor: factor }
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -66,7 +66,7 @@ module Legion
|
|
|
66
66
|
{ decayed: 0, archived: 0, error: e.message }
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
def check_corroboration(**) # rubocop:disable Metrics/CyclomaticComplexity
|
|
69
|
+
def check_corroboration(**) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
70
70
|
unless Helpers::DataModels.apollo_entry_available?
|
|
71
71
|
log.warn('Apollo Maintenance.check_corroboration skipped: apollo_data_not_available')
|
|
72
72
|
return { success: false, error: 'apollo_data_not_available' }
|
|
@@ -111,7 +111,7 @@ module Legion
|
|
|
111
111
|
to_entry_id: match.id,
|
|
112
112
|
relation_type: 'similar_to',
|
|
113
113
|
source_agent: 'system:corroboration',
|
|
114
|
-
weight:
|
|
114
|
+
weight: Legion::Extensions::Apollo.settings[:corroboration][:relation_weight]
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
promoted += 1
|
|
@@ -82,6 +82,7 @@ module Legion
|
|
|
82
82
|
force_decay_factor: 0.5
|
|
83
83
|
},
|
|
84
84
|
contradiction: {
|
|
85
|
+
enabled: false,
|
|
85
86
|
similar_limit: 10,
|
|
86
87
|
similarity_threshold: 0.7,
|
|
87
88
|
relation_weight: 0.8
|
|
@@ -129,6 +130,7 @@ module Legion
|
|
|
129
130
|
decay_interval: 3600,
|
|
130
131
|
expertise_interval: 1800,
|
|
131
132
|
corroboration_interval: 900,
|
|
133
|
+
contradiction_interval: 600,
|
|
132
134
|
entity_watchdog_interval: 120
|
|
133
135
|
}
|
|
134
136
|
}
|
|
@@ -102,7 +102,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Writeback do
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
it 'does not call Runners::Knowledge.handle_ingest directly' do
|
|
105
|
-
knowledge_mod = Module.new { def self.handle_ingest(**
|
|
105
|
+
knowledge_mod = Module.new { def self.handle_ingest(**); end }
|
|
106
106
|
stub_const('Legion::Extensions::Apollo::Runners', Module.new)
|
|
107
107
|
stub_const('Legion::Extensions::Apollo::Runners::Knowledge', knowledge_mod)
|
|
108
108
|
allow(knowledge_mod).to receive(:handle_ingest)
|
|
@@ -112,7 +112,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Writeback do
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
context 'when Legion::Apollo is not defined' do
|
|
115
|
-
let(:knowledge_mod) { Module.new { def self.handle_ingest(**
|
|
115
|
+
let(:knowledge_mod) { Module.new { def self.handle_ingest(**); end } }
|
|
116
116
|
|
|
117
117
|
before do
|
|
118
118
|
hide_const('Legion::Apollo') if defined?(Legion::Apollo)
|
|
@@ -42,7 +42,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::EntityExtractor do
|
|
|
42
42
|
stub_const('Legion::LLM', Module.new do
|
|
43
43
|
def self.started? = true
|
|
44
44
|
|
|
45
|
-
def self.structured(**
|
|
45
|
+
def self.structured(**) = { data: { entities: [] } }
|
|
46
46
|
end)
|
|
47
47
|
allow(Legion::LLM).to receive(:structured).and_return(llm_result)
|
|
48
48
|
end
|
|
@@ -77,7 +77,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::EntityExtractor do
|
|
|
77
77
|
stub_const('Legion::LLM', Module.new do
|
|
78
78
|
def self.started? = true
|
|
79
79
|
|
|
80
|
-
def self.structured(**
|
|
80
|
+
def self.structured(**) = raise(StandardError, 'timeout')
|
|
81
81
|
end)
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -371,7 +371,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
371
371
|
end
|
|
372
372
|
end
|
|
373
373
|
|
|
374
|
-
context 'identity
|
|
374
|
+
context 'identity from Legion::Identity::Process' do
|
|
375
375
|
let(:mock_entry_class2) { double('ApolloEntry2') }
|
|
376
376
|
let(:mock_expertise_class2) { double('ApolloExpertise2') }
|
|
377
377
|
let(:mock_access_log_class2) { double('ApolloAccessLog2') }
|
|
@@ -390,18 +390,20 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
390
390
|
allow(host).to receive(:embed_text).and_return(nil)
|
|
391
391
|
allow(host).to receive(:find_corroboration).and_return([false, nil])
|
|
392
392
|
allow(host).to receive(:detect_contradictions).and_return([])
|
|
393
|
+
stub_const('Legion::Identity::Process', double(
|
|
394
|
+
db_principal_id: 42,
|
|
395
|
+
db_identity_id: 7,
|
|
396
|
+
canonical_name: 'alice'
|
|
397
|
+
))
|
|
393
398
|
end
|
|
394
399
|
|
|
395
|
-
it '
|
|
400
|
+
it 'derives identity from Legion::Identity::Process and persists it' do
|
|
396
401
|
expect(mock_entry_class2).to receive(:create).with(
|
|
397
|
-
hash_including(identity_principal_id: 42,
|
|
402
|
+
hash_including(identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice',
|
|
403
|
+
access_scope: 'private')
|
|
398
404
|
).and_return(mock_entry2)
|
|
399
405
|
|
|
400
|
-
host.handle_ingest(
|
|
401
|
-
content: 'test fact', tags: [],
|
|
402
|
-
identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice',
|
|
403
|
-
access_scope: 'private'
|
|
404
|
-
)
|
|
406
|
+
host.handle_ingest(content: 'test fact', tags: [], access_scope: 'private')
|
|
405
407
|
end
|
|
406
408
|
|
|
407
409
|
it 'defaults access_scope to global when not provided' do
|
|
@@ -412,14 +414,14 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
412
414
|
host.handle_ingest(content: 'test fact', tags: [])
|
|
413
415
|
end
|
|
414
416
|
|
|
415
|
-
it '
|
|
417
|
+
it 'ignores identity kwargs passed by callers' do
|
|
416
418
|
expect(mock_entry_class2).to receive(:create).with(
|
|
417
|
-
hash_including(identity_id: 7, identity_canonical_name: 'alice')
|
|
419
|
+
hash_including(identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice')
|
|
418
420
|
).and_return(mock_entry2)
|
|
419
421
|
|
|
420
422
|
host.handle_ingest(
|
|
421
423
|
content: 'test fact', tags: [],
|
|
422
|
-
identity_principal_id:
|
|
424
|
+
identity_principal_id: 999, identity_id: 888, identity_canonical_name: 'mallory',
|
|
423
425
|
access_scope: 'private'
|
|
424
426
|
)
|
|
425
427
|
end
|
|
@@ -490,6 +492,88 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
490
492
|
end
|
|
491
493
|
end
|
|
492
494
|
end
|
|
495
|
+
|
|
496
|
+
context 'contradiction detection flag' do
|
|
497
|
+
let(:mock_entry_class) { double('ApolloEntry') }
|
|
498
|
+
let(:mock_relation_class) { double('ApolloRelation') }
|
|
499
|
+
let(:mock_expertise_class) { double('ApolloExpertise') }
|
|
500
|
+
let(:mock_access_log_class) { double('ApolloAccessLog') }
|
|
501
|
+
let(:mock_entry) { double('entry', id: 'uuid-new', embedding: nil) }
|
|
502
|
+
let(:empty_dataset) { double('dataset', each: nil) }
|
|
503
|
+
let(:mock_db) { double('db') }
|
|
504
|
+
|
|
505
|
+
before do
|
|
506
|
+
stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
|
|
507
|
+
stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
|
|
508
|
+
stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
|
|
509
|
+
stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
|
|
510
|
+
allow(Legion::LLM::Call::Embeddings).to receive(:generate)
|
|
511
|
+
.and_return({ vector: Array.new(1024, 0.1), model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 })
|
|
512
|
+
allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: empty_dataset)))
|
|
513
|
+
dedup_chain = double('dedup_chain')
|
|
514
|
+
allow(mock_entry_class).to receive(:where).with(content_hash: anything).and_return(dedup_chain)
|
|
515
|
+
allow(dedup_chain).to receive(:exclude).with(status: 'archived').and_return(double(first: nil))
|
|
516
|
+
allow(mock_entry_class).to receive(:db).and_return(mock_db)
|
|
517
|
+
allow(mock_db).to receive(:fetch).and_return(double(all: []))
|
|
518
|
+
allow(mock_entry_class).to receive(:create).and_return(mock_entry)
|
|
519
|
+
allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
|
|
520
|
+
allow(mock_expertise_class).to receive(:create)
|
|
521
|
+
allow(mock_access_log_class).to receive(:create)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it 'defaults contradiction detection to disabled' do
|
|
525
|
+
expect(Legion::Extensions::Apollo.settings[:contradiction][:enabled]).to be false
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it 'does not call detect_contradictions when flag is false' do
|
|
529
|
+
expect(host).not_to receive(:detect_contradictions)
|
|
530
|
+
result = host.handle_ingest(content: 'test fact', content_type: 'fact', source_agent: 'agent-1')
|
|
531
|
+
expect(result[:success]).to be true
|
|
532
|
+
expect(result[:contradictions]).to eq([])
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 'returns stable contradictions key as empty array when disabled' do
|
|
536
|
+
result = host.handle_ingest(content: 'test content', content_type: 'fact', source_agent: 'agent-1')
|
|
537
|
+
expect(result).to have_key(:contradictions)
|
|
538
|
+
expect(result[:contradictions]).to eq([])
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
context 'when enabled' do
|
|
542
|
+
before do
|
|
543
|
+
Legion::Settings[:extensions][:apollo][:contradiction][:enabled] = true
|
|
544
|
+
stub_const('Legion::Extensions::Apollo::Actor::ContradictionScanner',
|
|
545
|
+
Class.new do
|
|
546
|
+
@queue = Queue.new
|
|
547
|
+
class << self
|
|
548
|
+
attr_reader :queue
|
|
549
|
+
|
|
550
|
+
def enqueue(entry_id:, embedding:, content:)
|
|
551
|
+
queue.push({ entry_id: entry_id, embedding: embedding, content: content })
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def pending_count = queue.size
|
|
555
|
+
|
|
556
|
+
def drain
|
|
557
|
+
items = []
|
|
558
|
+
items << queue.pop until queue.empty?
|
|
559
|
+
items
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
after do
|
|
566
|
+
Legion::Settings[:extensions][:apollo][:contradiction][:enabled] = false
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it 'enqueues contradiction work to the actor instead of running inline' do
|
|
570
|
+
result = host.handle_ingest(content: 'enabled test', content_type: 'fact', source_agent: 'agent-1')
|
|
571
|
+
expect(result[:success]).to be true
|
|
572
|
+
expect(result[:contradictions]).to eq([])
|
|
573
|
+
expect(Legion::Extensions::Apollo::Actor::ContradictionScanner.pending_count).to eq(1)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
493
577
|
end
|
|
494
578
|
|
|
495
579
|
describe '#handle_query' do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-apollo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- CHANGELOG.md
|
|
161
161
|
- README.md
|
|
162
162
|
- lib/legion/extensions/apollo.rb
|
|
163
|
+
- lib/legion/extensions/apollo/actors/contradiction_scanner.rb
|
|
163
164
|
- lib/legion/extensions/apollo/actors/corroboration_checker.rb
|
|
164
165
|
- lib/legion/extensions/apollo/actors/decay.rb
|
|
165
166
|
- lib/legion/extensions/apollo/actors/entity_watchdog.rb
|