lex-apollo 0.4.28 → 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 +28 -12
- 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 +82 -0
- 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)
|
|
@@ -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 }
|
|
@@ -232,7 +232,7 @@ module Legion
|
|
|
232
232
|
{ success: false, error: e.message }
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
def redistribute_knowledge(agent_id:, min_confidence:
|
|
235
|
+
def redistribute_knowledge(agent_id:, min_confidence: settings[:query][:redistribute_min_confidence], **)
|
|
236
236
|
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
237
237
|
|
|
238
238
|
log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
|
|
@@ -267,7 +267,7 @@ module Legion
|
|
|
267
267
|
{ success: false, error: e.message }
|
|
268
268
|
end
|
|
269
269
|
|
|
270
|
-
def retrieve_relevant(query: nil, limit:
|
|
270
|
+
def retrieve_relevant(query: nil, limit: settings[:query][:retrieval_limit],
|
|
271
271
|
min_confidence: Helpers::GraphQuery.default_query_min_confidence,
|
|
272
272
|
tags: nil, domain: nil, skip: false, requesting_principal_id: nil, **)
|
|
273
273
|
return { status: :skipped } if skip
|
|
@@ -316,7 +316,7 @@ module Legion
|
|
|
316
316
|
{ success: false, error: e.message }
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
-
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
|
|
320
320
|
unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
|
|
321
321
|
return { success: false, error: 'apollo_data_not_available' }
|
|
322
322
|
end
|
|
@@ -596,12 +596,28 @@ module Legion
|
|
|
596
596
|
Array(allowed)
|
|
597
597
|
end
|
|
598
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
|
+
|
|
599
615
|
def detect_contradictions(entry_id, embedding, content)
|
|
600
616
|
return [] unless embedding && Helpers::DataModels.apollo_entry_available?
|
|
601
617
|
|
|
602
|
-
sim_limit =
|
|
603
|
-
sim_threshold =
|
|
604
|
-
rel_weight =
|
|
618
|
+
sim_limit = settings[:contradiction][:similar_limit]
|
|
619
|
+
sim_threshold = settings[:contradiction][:similarity_threshold]
|
|
620
|
+
rel_weight = settings[:contradiction][:relation_weight]
|
|
605
621
|
|
|
606
622
|
db = Helpers::DataModels.apollo_entry.db
|
|
607
623
|
log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
|
|
@@ -658,7 +674,7 @@ module Legion
|
|
|
658
674
|
def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
|
|
659
675
|
return [false, nil] unless embedding
|
|
660
676
|
|
|
661
|
-
scan_limit =
|
|
677
|
+
scan_limit = settings[:corroboration][:scan_limit]
|
|
662
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
|
|
663
679
|
existing = Helpers::DataModels.apollo_entry
|
|
664
680
|
.where(content_type: content_type_sym)
|
|
@@ -671,7 +687,7 @@ module Legion
|
|
|
671
687
|
sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: entry.embedding)
|
|
672
688
|
next unless Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
|
|
673
689
|
|
|
674
|
-
same_provider_wt =
|
|
690
|
+
same_provider_wt = settings[:corroboration][:same_provider_weight]
|
|
675
691
|
weight = same_source_provider?(source_agent, entry) ? same_provider_wt : 1.0
|
|
676
692
|
|
|
677
693
|
# Reject corroboration entirely if same channel (same data source)
|
|
@@ -716,7 +732,7 @@ module Legion
|
|
|
716
732
|
else
|
|
717
733
|
Helpers::DataModels.apollo_expertise.create(
|
|
718
734
|
agent_id: source_agent, domain: domain,
|
|
719
|
-
proficiency:
|
|
735
|
+
proficiency: settings[:expertise][:initial_proficiency],
|
|
720
736
|
entry_count: 1, last_active_at: Time.now
|
|
721
737
|
)
|
|
722
738
|
end
|
|
@@ -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
|
|
|
@@ -492,6 +492,88 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
|
|
|
492
492
|
end
|
|
493
493
|
end
|
|
494
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
|
|
495
577
|
end
|
|
496
578
|
|
|
497
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
|