lex-apollo 0.4.22 → 0.4.24
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/extensions/apollo/actors/corroboration_checker.rb +3 -1
- data/lib/legion/extensions/apollo/actors/decay.rb +3 -1
- data/lib/legion/extensions/apollo/actors/entity_watchdog.rb +10 -21
- data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +3 -1
- data/lib/legion/extensions/apollo/actors/gas_subscriber.rb +1 -1
- data/lib/legion/extensions/apollo/actors/ingest.rb +1 -1
- data/lib/legion/extensions/apollo/actors/query_responder.rb +1 -1
- data/lib/legion/extensions/apollo/actors/writeback_store.rb +1 -1
- data/lib/legion/extensions/apollo/actors/writeback_vectorize.rb +2 -2
- data/lib/legion/extensions/apollo/api.rb +28 -22
- data/lib/legion/extensions/apollo/gaia_integration.rb +16 -13
- data/lib/legion/extensions/apollo/helpers/capability.rb +19 -17
- data/lib/legion/extensions/apollo/helpers/confidence.rb +5 -8
- data/lib/legion/extensions/apollo/helpers/data_models.rb +61 -0
- data/lib/legion/extensions/apollo/helpers/entity_watchdog.rb +8 -15
- data/lib/legion/extensions/apollo/helpers/similarity.rb +5 -6
- data/lib/legion/extensions/apollo/helpers/writeback.rb +13 -14
- data/lib/legion/extensions/apollo/runners/expertise.rb +10 -8
- data/lib/legion/extensions/apollo/runners/gas.rb +47 -27
- data/lib/legion/extensions/apollo/runners/knowledge.rb +95 -80
- data/lib/legion/extensions/apollo/runners/maintenance.rb +7 -5
- data/lib/legion/extensions/apollo/runners/request.rb +7 -1
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/lib/legion/extensions/apollo.rb +96 -0
- data/spec/legion/extensions/apollo/actors/writeback_vectorize_spec.rb +3 -3
- data/spec/legion/extensions/apollo/api_spec.rb +20 -0
- data/spec/legion/extensions/apollo/helpers/capability_spec.rb +4 -4
- data/spec/legion/extensions/apollo/runners/gas_anticipate_spec.rb +0 -3
- data/spec/legion/extensions/apollo/runners/gas_relate_spec.rb +0 -4
- data/spec/legion/extensions/apollo/runners/gas_synthesize_spec.rb +0 -11
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +25 -15
- data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +8 -8
- data/spec/legion/extensions/apollo/runners/request_spec.rb +8 -8
- data/spec/spec_helper.rb +4 -0
- metadata +2 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
3
|
+
require 'legion/json'
|
|
4
|
+
require 'legion/settings'
|
|
4
5
|
require_relative '../helpers/confidence'
|
|
6
|
+
require_relative '../helpers/data_models'
|
|
5
7
|
|
|
6
8
|
module Legion
|
|
7
9
|
module Extensions
|
|
@@ -26,9 +28,9 @@ module Legion
|
|
|
26
28
|
|
|
27
29
|
def store_knowledge(content:, content_type:, tags: [], source_agent: nil, context: {}, **)
|
|
28
30
|
content_type = normalize_content_type(content_type)
|
|
29
|
-
log.debug("Apollo Knowledge.store_knowledge content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent || 'nil'} data_available=#{
|
|
31
|
+
log.debug("Apollo Knowledge.store_knowledge content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent || 'nil'} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
|
|
30
32
|
|
|
31
|
-
if
|
|
33
|
+
if Helpers::DataModels.apollo_entry_available?
|
|
32
34
|
return handle_ingest(content: content, content_type: content_type,
|
|
33
35
|
tags: Array(tags), source_agent: source_agent, context: context, **)
|
|
34
36
|
end
|
|
@@ -44,8 +46,8 @@ module Legion
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: %i[confirmed candidate], tags: nil, **) # rubocop:disable Layout/LineLength
|
|
47
|
-
log.debug("Apollo Knowledge.query_knowledge query_length=#{query.to_s.length} limit=#{limit} statuses=#{Array(status).join(',')} tags=#{Array(tags).size} data_available=#{
|
|
48
|
-
if
|
|
49
|
+
log.debug("Apollo Knowledge.query_knowledge query_length=#{query.to_s.length} limit=#{limit} statuses=#{Array(status).join(',')} tags=#{Array(tags).size} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
|
|
50
|
+
if Helpers::DataModels.apollo_entry_available?
|
|
49
51
|
return handle_query(query: query, limit: limit, min_confidence: min_confidence,
|
|
50
52
|
status: status, tags: tags, **)
|
|
51
53
|
end
|
|
@@ -61,8 +63,8 @@ module Legion
|
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def related_entries(entry_id:, relation_types: nil, depth: Helpers::GraphQuery.default_depth, **)
|
|
64
|
-
log.debug("Apollo Knowledge.related_entries entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} data_available=#{
|
|
65
|
-
return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if
|
|
66
|
+
log.debug("Apollo Knowledge.related_entries entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
|
|
67
|
+
return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if Helpers::DataModels.apollo_entry_available?
|
|
66
68
|
|
|
67
69
|
{
|
|
68
70
|
action: :traverse,
|
|
@@ -84,6 +86,7 @@ module Legion
|
|
|
84
86
|
return { status: :skipped } if skip
|
|
85
87
|
|
|
86
88
|
content = normalize_text_input(content)
|
|
89
|
+
content_type = normalize_content_type(content_type.nil? ? :observation : content_type)
|
|
87
90
|
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
91
|
early_error = ingest_early_return_error(content: content, content_type: content_type, tags: tags)
|
|
89
92
|
return early_error if early_error
|
|
@@ -116,7 +119,7 @@ module Legion
|
|
|
116
119
|
|
|
117
120
|
upsert_expertise(source_agent: metadata[:source_agent], domain: metadata[:domain])
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
Helpers::DataModels.apollo_access_log.create(
|
|
120
123
|
entry_id: existing_id, agent_id: metadata[:source_agent], action: 'ingest'
|
|
121
124
|
)
|
|
122
125
|
|
|
@@ -130,9 +133,10 @@ module Legion
|
|
|
130
133
|
{ success: false, error: e.message }
|
|
131
134
|
end
|
|
132
135
|
|
|
133
|
-
def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: UNSET, tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength
|
|
134
|
-
return { success: false, error: 'apollo_data_not_available' } unless
|
|
136
|
+
def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: UNSET, tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength, Metrics/CyclomaticComplexity
|
|
137
|
+
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
135
138
|
|
|
139
|
+
entry_model = Helpers::DataModels.apollo_entry
|
|
136
140
|
query = normalize_text_input(query)
|
|
137
141
|
status_defaulted = status.equal?(UNSET)
|
|
138
142
|
requested_status = status_defaulted ? DEFAULT_QUERY_STATUS : status
|
|
@@ -143,31 +147,27 @@ module Legion
|
|
|
143
147
|
end
|
|
144
148
|
|
|
145
149
|
embedding = embed_text(query)
|
|
150
|
+
if embedding.nil?
|
|
151
|
+
log.warn('Apollo Knowledge.handle_query embedding unavailable; falling back to browse query')
|
|
152
|
+
return list_entries_chronologically(query: query, limit: limit, status: requested_status,
|
|
153
|
+
status_defaulted: status_defaulted, tags: tags, domain: domain)
|
|
154
|
+
end
|
|
155
|
+
|
|
146
156
|
sql = Helpers::GraphQuery.build_semantic_search_sql(
|
|
147
157
|
limit: limit, min_confidence: min_confidence,
|
|
148
158
|
statuses: Array(requested_status).map(&:to_s), tags: tags, domain: domain
|
|
149
159
|
)
|
|
150
160
|
|
|
151
|
-
db =
|
|
161
|
+
db = entry_model.db
|
|
152
162
|
entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
|
|
153
163
|
|
|
154
164
|
entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
|
|
155
165
|
|
|
156
166
|
entries.each do |entry|
|
|
157
|
-
|
|
158
|
-
access_count: Sequel.expr(:access_count) + 1,
|
|
159
|
-
confidence: Helpers::Confidence.apply_retrieval_boost(
|
|
160
|
-
confidence: entry[:confidence]
|
|
161
|
-
),
|
|
162
|
-
updated_at: Time.now
|
|
163
|
-
)
|
|
167
|
+
boost_entry_after_query(entry_model, entry)
|
|
164
168
|
end
|
|
165
169
|
|
|
166
|
-
if entries.any?
|
|
167
|
-
Legion::Data::Model::ApolloAccessLog.create(
|
|
168
|
-
entry_id: entries.first&.dig(:id), agent_id: agent_id, action: 'query'
|
|
169
|
-
)
|
|
170
|
-
end
|
|
170
|
+
record_query_access(entry_id: entries.first&.dig(:id), agent_id: agent_id) if entries.any?
|
|
171
171
|
|
|
172
172
|
formatted = entries.map do |entry|
|
|
173
173
|
{ id: entry[:id], content: entry[:content], content_type: entry[:content_type],
|
|
@@ -184,7 +184,7 @@ module Legion
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
def handle_traverse(entry_id:, depth: Helpers::GraphQuery.default_depth, relation_types: nil, agent_id: 'unknown', **)
|
|
187
|
-
return { success: false, error: 'apollo_data_not_available' } unless
|
|
187
|
+
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
188
188
|
|
|
189
189
|
log.debug("Apollo Knowledge.handle_traverse entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} agent_id=#{agent_id}") # rubocop:disable Layout/LineLength
|
|
190
190
|
# Whitelist relation_types to prevent SQL injection (they are string-interpolated in build_traversal_sql)
|
|
@@ -194,11 +194,11 @@ module Legion
|
|
|
194
194
|
end
|
|
195
195
|
|
|
196
196
|
sql = Helpers::GraphQuery.build_traversal_sql(depth: depth, relation_types: relation_types)
|
|
197
|
-
db =
|
|
197
|
+
db = Helpers::DataModels.apollo_entry.db
|
|
198
198
|
entries = db.fetch(sql, entry_id: entry_id).all
|
|
199
199
|
|
|
200
200
|
if entries.any? && agent_id != 'unknown'
|
|
201
|
-
|
|
201
|
+
Helpers::DataModels.apollo_access_log.create(
|
|
202
202
|
entry_id: entry_id, agent_id: agent_id, action: 'query'
|
|
203
203
|
)
|
|
204
204
|
end
|
|
@@ -217,13 +217,13 @@ module Legion
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def redistribute_knowledge(agent_id:, min_confidence: Helpers::Confidence.apollo_setting(:query, :redistribute_min_confidence, default: 0.5), **)
|
|
220
|
-
return { success: false, error: 'apollo_data_not_available' } unless
|
|
220
|
+
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
221
221
|
|
|
222
222
|
log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
|
|
223
|
-
entries =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
entries = Helpers::DataModels.apollo_entry
|
|
224
|
+
.where(source_agent: agent_id, status: 'confirmed')
|
|
225
|
+
.where { confidence > min_confidence }
|
|
226
|
+
.all
|
|
227
227
|
|
|
228
228
|
return { success: true, redistributed: 0 } if entries.empty?
|
|
229
229
|
|
|
@@ -254,24 +254,29 @@ module Legion
|
|
|
254
254
|
def retrieve_relevant(query: nil, limit: Helpers::Confidence.apollo_setting(:query, :retrieval_limit, default: 5), min_confidence: Helpers::GraphQuery.default_query_min_confidence, tags: nil, domain: nil, skip: false, **) # rubocop:disable Layout/LineLength
|
|
255
255
|
return { status: :skipped } if skip
|
|
256
256
|
|
|
257
|
-
return { success: false, error: 'apollo_data_not_available' } unless
|
|
257
|
+
return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
|
|
258
258
|
|
|
259
259
|
query = normalize_text_input(query)
|
|
260
260
|
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
|
|
261
261
|
return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
|
|
262
262
|
|
|
263
263
|
embedding = embed_text(query)
|
|
264
|
+
if embedding.nil?
|
|
265
|
+
log.warn('Apollo Knowledge.retrieve_relevant embedding unavailable; returning empty result')
|
|
266
|
+
return { success: true, entries: [], count: 0, degraded: :no_embedding }
|
|
267
|
+
end
|
|
268
|
+
|
|
264
269
|
sql = Helpers::GraphQuery.build_semantic_search_sql(
|
|
265
270
|
limit: limit, min_confidence: min_confidence,
|
|
266
271
|
statuses: %w[confirmed candidate], tags: tags, domain: domain
|
|
267
272
|
)
|
|
268
273
|
|
|
269
|
-
db =
|
|
274
|
+
db = Helpers::DataModels.apollo_entry.db
|
|
270
275
|
entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
|
|
271
276
|
entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
|
|
272
277
|
|
|
273
278
|
entries.each do |entry|
|
|
274
|
-
|
|
279
|
+
Helpers::DataModels.apollo_entry.where(id: entry[:id]).update(
|
|
275
280
|
confidence: Helpers::Confidence.apply_retrieval_boost(confidence: entry[:confidence]),
|
|
276
281
|
updated_at: Time.now
|
|
277
282
|
)
|
|
@@ -360,13 +365,7 @@ module Legion
|
|
|
360
365
|
return { success: false, error: 'content is required' }
|
|
361
366
|
end
|
|
362
367
|
|
|
363
|
-
if
|
|
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)
|
|
368
|
+
return nil if Helpers::DataModels.apollo_entry_available?
|
|
370
369
|
|
|
371
370
|
log.warn('[apollo][handle_ingest] early-return: apollo_data_not_available ' \
|
|
372
371
|
"content_type=#{content_type}")
|
|
@@ -382,18 +381,18 @@ module Legion
|
|
|
382
381
|
def embed_text(text)
|
|
383
382
|
text = normalize_text_input(text)
|
|
384
383
|
log.debug("Apollo Knowledge.embed_text text_length=#{text.length}")
|
|
385
|
-
result = Legion::LLM::Embeddings.generate(text: text)
|
|
384
|
+
result = Legion::LLM::Call::Embeddings.generate(text: text)
|
|
386
385
|
vector = result.is_a?(Hash) ? result[:vector] : result
|
|
387
|
-
if vector.is_a?(Array) && vector.any?
|
|
386
|
+
if vector.is_a?(Array) && vector.any? && vector.any? { |v| v != 0.0 }
|
|
388
387
|
log.debug("Apollo Knowledge.embed_text vector_dimensions=#{vector.length}")
|
|
389
388
|
vector
|
|
390
389
|
else
|
|
391
|
-
log.warn('Apollo Knowledge.embed_text returned no vector;
|
|
392
|
-
|
|
390
|
+
log.warn('Apollo Knowledge.embed_text returned no usable vector; skipping embedding')
|
|
391
|
+
nil
|
|
393
392
|
end
|
|
394
393
|
rescue StandardError => e
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.embed_text')
|
|
395
|
+
nil
|
|
397
396
|
end
|
|
398
397
|
|
|
399
398
|
def normalize_text_input(value)
|
|
@@ -405,7 +404,7 @@ module Legion
|
|
|
405
404
|
|
|
406
405
|
sanitize_for_postgres(result)
|
|
407
406
|
rescue StandardError => e
|
|
408
|
-
|
|
407
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.normalize_text_input')
|
|
409
408
|
''
|
|
410
409
|
end
|
|
411
410
|
|
|
@@ -429,10 +428,10 @@ module Legion
|
|
|
429
428
|
def active_duplicate_for_hash(hash)
|
|
430
429
|
return nil unless hash
|
|
431
430
|
|
|
432
|
-
existing =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
431
|
+
existing = Helpers::DataModels.apollo_entry
|
|
432
|
+
.where(content_hash: hash)
|
|
433
|
+
.exclude(status: 'archived')
|
|
434
|
+
.first
|
|
436
435
|
existing&.update(confidence: [existing.confidence + Helpers::Confidence.retrieval_boost, 1.0].min)
|
|
437
436
|
log.debug("Apollo Knowledge.active_duplicate_for_hash matched entry_id=#{existing.id}") if existing
|
|
438
437
|
existing
|
|
@@ -452,21 +451,21 @@ module Legion
|
|
|
452
451
|
end
|
|
453
452
|
|
|
454
453
|
def create_candidate_entry(content:, content_type:, context:, metadata:, content_hash:, embedding:)
|
|
455
|
-
new_entry =
|
|
454
|
+
new_entry = Helpers::DataModels.apollo_entry.create(
|
|
456
455
|
content: content,
|
|
457
456
|
content_type: content_type,
|
|
458
457
|
confidence: Helpers::Confidence.initial_confidence,
|
|
459
458
|
source_agent: metadata[:source_agent],
|
|
460
459
|
source_provider: metadata[:source_provider],
|
|
461
460
|
source_channel: metadata[:source_channel],
|
|
462
|
-
source_context:
|
|
461
|
+
source_context: json_dump(context.is_a?(Hash) ? context : {}),
|
|
463
462
|
tags: Sequel.pg_array(metadata[:tags]),
|
|
464
463
|
status: 'candidate',
|
|
465
464
|
knowledge_domain: metadata[:domain],
|
|
466
465
|
submitted_by: metadata[:submitted_by],
|
|
467
466
|
submitted_from: metadata[:submitted_from],
|
|
468
467
|
content_hash: content_hash,
|
|
469
|
-
embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")
|
|
468
|
+
embedding: embedding ? Sequel.lit("'[#{embedding.join(',')}]'::vector") : nil
|
|
470
469
|
)
|
|
471
470
|
log.info("Apollo Knowledge.handle_ingest created entry_id=#{new_entry.id} status=candidate domain=#{metadata[:domain]} source_agent=#{metadata[:source_agent]}") # rubocop:disable Layout/LineLength
|
|
472
471
|
new_entry.id
|
|
@@ -478,7 +477,7 @@ module Legion
|
|
|
478
477
|
|
|
479
478
|
def list_entries_chronologically(query:, limit:, status:, status_defaulted:, tags:, domain:)
|
|
480
479
|
log.debug("Apollo Knowledge.list_entries_chronologically limit=#{limit} statuses=#{Array(status).join(',')} status_defaulted=#{status_defaulted} tags=#{Array(tags).size} domain=#{domain || 'nil'}") # rubocop:disable Layout/LineLength
|
|
481
|
-
dataset =
|
|
480
|
+
dataset = Helpers::DataModels.apollo_entry.exclude(status: 'archived')
|
|
482
481
|
requested = Array(status).map(&:to_s).reject(&:empty?)
|
|
483
482
|
dataset = dataset.where(status: requested) unless status_defaulted || requested.empty?
|
|
484
483
|
dataset = dataset.where(Sequel.lit('tags && ?', Sequel.pg_array(Array(tags)))) if tags && !Array(tags).empty?
|
|
@@ -501,12 +500,28 @@ module Legion
|
|
|
501
500
|
knowledge_domain: entry[:knowledge_domain] }
|
|
502
501
|
end
|
|
503
502
|
|
|
503
|
+
def record_query_access(entry_id:, agent_id:)
|
|
504
|
+
Helpers::DataModels.apollo_access_log.create(
|
|
505
|
+
entry_id: entry_id, agent_id: agent_id, action: 'query'
|
|
506
|
+
)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def boost_entry_after_query(entry_model, entry)
|
|
510
|
+
entry_model.where(id: entry[:id]).update(
|
|
511
|
+
access_count: Sequel.expr(:access_count) + 1,
|
|
512
|
+
confidence: Helpers::Confidence.apply_retrieval_boost(
|
|
513
|
+
confidence: entry[:confidence]
|
|
514
|
+
),
|
|
515
|
+
updated_at: Time.now
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def settings
|
|
520
|
+
Legion::Extensions::Apollo.settings
|
|
521
|
+
end
|
|
522
|
+
|
|
504
523
|
def allowed_domains_for(target_domain)
|
|
505
|
-
rules =
|
|
506
|
-
Legion::Settings.dig(:apollo, :domain_isolation)
|
|
507
|
-
else
|
|
508
|
-
DOMAIN_ISOLATION
|
|
509
|
-
end
|
|
524
|
+
rules = settings[:domain_isolation]
|
|
510
525
|
|
|
511
526
|
allowed = rules[target_domain]
|
|
512
527
|
return :all if allowed == :all || allowed.nil?
|
|
@@ -515,13 +530,13 @@ module Legion
|
|
|
515
530
|
end
|
|
516
531
|
|
|
517
532
|
def detect_contradictions(entry_id, embedding, content)
|
|
518
|
-
return [] unless embedding &&
|
|
533
|
+
return [] unless embedding && Helpers::DataModels.apollo_entry_available?
|
|
519
534
|
|
|
520
535
|
sim_limit = Helpers::Confidence.apollo_setting(:contradiction, :similar_limit, default: 10)
|
|
521
536
|
sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
|
|
522
537
|
rel_weight = Helpers::Confidence.apollo_setting(:contradiction, :relation_weight, default: 0.8)
|
|
523
538
|
|
|
524
|
-
db =
|
|
539
|
+
db = Helpers::DataModels.apollo_entry.db
|
|
525
540
|
log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
|
|
526
541
|
similar = db.fetch(
|
|
527
542
|
"SELECT id, content, embedding FROM apollo_entries WHERE id != :entry_id AND embedding IS NOT NULL ORDER BY embedding <=> :embedding LIMIT #{sim_limit}", # rubocop:disable Layout/LineLength
|
|
@@ -538,13 +553,13 @@ module Legion
|
|
|
538
553
|
next unless sim > sim_threshold
|
|
539
554
|
next unless llm_detects_conflict?(content, existing[:content])
|
|
540
555
|
|
|
541
|
-
|
|
556
|
+
Helpers::DataModels.apollo_relation.create(
|
|
542
557
|
from_entry_id: entry_id, to_entry_id: existing[:id],
|
|
543
558
|
relation_type: 'contradicts', source_agent: 'system:contradiction',
|
|
544
559
|
weight: rel_weight
|
|
545
560
|
)
|
|
546
561
|
|
|
547
|
-
|
|
562
|
+
Helpers::DataModels.apollo_entry.where(id: [entry_id, existing[:id]]).update(status: 'disputed')
|
|
548
563
|
contradictions << existing[:id]
|
|
549
564
|
end
|
|
550
565
|
log.info("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} contradictions=#{contradictions.size}") if contradictions.any?
|
|
@@ -569,17 +584,19 @@ module Legion
|
|
|
569
584
|
)
|
|
570
585
|
result[:data]&.dig(:contradicts) == true
|
|
571
586
|
rescue StandardError => e
|
|
572
|
-
|
|
587
|
+
handle_exception(e, level: :warn, operation: 'apollo.knowledge.llm_detects_conflict')
|
|
573
588
|
false
|
|
574
589
|
end
|
|
575
590
|
|
|
576
591
|
def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
|
|
592
|
+
return [false, nil] unless embedding
|
|
593
|
+
|
|
577
594
|
scan_limit = Helpers::Confidence.apollo_setting(:corroboration, :scan_limit, default: 50)
|
|
578
595
|
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
|
|
579
|
-
existing =
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
596
|
+
existing = Helpers::DataModels.apollo_entry
|
|
597
|
+
.where(content_type: content_type_sym)
|
|
598
|
+
.exclude(embedding: nil)
|
|
599
|
+
.limit(scan_limit)
|
|
583
600
|
|
|
584
601
|
existing.each do |entry|
|
|
585
602
|
next unless entry.embedding
|
|
@@ -601,13 +618,6 @@ module Legion
|
|
|
601
618
|
confidence: Helpers::Confidence.apply_corroboration_boost(confidence: entry.confidence, weight: weight),
|
|
602
619
|
updated_at: Time.now
|
|
603
620
|
)
|
|
604
|
-
Legion::Data::Model::ApolloRelation.create(
|
|
605
|
-
from_entry_id: entry.id,
|
|
606
|
-
to_entry_id: entry.id,
|
|
607
|
-
relation_type: 'similar_to',
|
|
608
|
-
source_agent: source_agent,
|
|
609
|
-
weight: sim
|
|
610
|
-
)
|
|
611
621
|
log.info("Apollo Knowledge.find_corroboration matched entry_id=#{entry.id} source_agent=#{source_agent} similarity=#{sim}")
|
|
612
622
|
return [true, entry.id]
|
|
613
623
|
end
|
|
@@ -632,12 +642,12 @@ module Legion
|
|
|
632
642
|
|
|
633
643
|
def upsert_expertise(source_agent:, domain:)
|
|
634
644
|
log.debug("Apollo Knowledge.upsert_expertise source_agent=#{source_agent} domain=#{domain}")
|
|
635
|
-
expertise =
|
|
636
|
-
|
|
645
|
+
expertise = Helpers::DataModels.apollo_expertise
|
|
646
|
+
.where(agent_id: source_agent, domain: domain).first
|
|
637
647
|
if expertise
|
|
638
648
|
expertise.update(entry_count: expertise.entry_count + 1, last_active_at: Time.now)
|
|
639
649
|
else
|
|
640
|
-
|
|
650
|
+
Helpers::DataModels.apollo_expertise.create(
|
|
641
651
|
agent_id: source_agent, domain: domain,
|
|
642
652
|
proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0),
|
|
643
653
|
entry_count: 1, last_active_at: Time.now
|
|
@@ -646,6 +656,11 @@ module Legion
|
|
|
646
656
|
end
|
|
647
657
|
|
|
648
658
|
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
659
|
+
include Legion::JSON::Helper
|
|
660
|
+
include Legion::Settings::Helper
|
|
661
|
+
extend Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
662
|
+
extend Legion::JSON::Helper
|
|
663
|
+
extend Legion::Settings::Helper
|
|
649
664
|
end
|
|
650
665
|
end
|
|
651
666
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../helpers/confidence'
|
|
4
|
+
require_relative '../helpers/data_models'
|
|
4
5
|
require_relative '../helpers/similarity'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
@@ -42,7 +43,8 @@ module Legion
|
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
min_age_filter = Sequel.lit(
|
|
45
|
-
"COALESCE(updated_at, created_at) < NOW() - INTERVAL '
|
|
46
|
+
"COALESCE(updated_at, created_at) < NOW() - (? * INTERVAL '1 hour')",
|
|
47
|
+
min_age_hours
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
decayed = conn[:apollo_entries]
|
|
@@ -65,13 +67,13 @@ module Legion
|
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def check_corroboration(**) # rubocop:disable Metrics/CyclomaticComplexity
|
|
68
|
-
unless
|
|
70
|
+
unless Helpers::DataModels.apollo_entry_available?
|
|
69
71
|
log.warn('Apollo Maintenance.check_corroboration skipped: apollo_data_not_available')
|
|
70
72
|
return { success: false, error: 'apollo_data_not_available' }
|
|
71
73
|
end
|
|
72
74
|
|
|
73
|
-
candidates =
|
|
74
|
-
confirmed =
|
|
75
|
+
candidates = Helpers::DataModels.apollo_entry.where(status: 'candidate').exclude(embedding: nil).limit(100).all
|
|
76
|
+
confirmed = Helpers::DataModels.apollo_entry.where(status: 'confirmed').exclude(embedding: nil).limit(500).all
|
|
75
77
|
log.debug("Apollo Maintenance.check_corroboration candidates=#{candidates.size} confirmed=#{confirmed.size}")
|
|
76
78
|
|
|
77
79
|
promoted = 0
|
|
@@ -104,7 +106,7 @@ module Legion
|
|
|
104
106
|
updated_at: Time.now
|
|
105
107
|
)
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
Helpers::DataModels.apollo_relation.create(
|
|
108
110
|
from_entry_id: candidate.id,
|
|
109
111
|
to_entry_id: match.id,
|
|
110
112
|
relation_type: 'similar_to',
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../helpers/data_models'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module Apollo
|
|
6
8
|
module Runners
|
|
7
9
|
module Request
|
|
10
|
+
include Legion::Logging::Helper
|
|
11
|
+
extend Legion::Logging::Helper
|
|
8
12
|
extend self
|
|
9
13
|
|
|
10
14
|
def self.data_required?
|
|
@@ -67,7 +71,7 @@ module Legion
|
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def local_service_available?
|
|
70
|
-
|
|
74
|
+
Helpers::DataModels.apollo_entry_available? &&
|
|
71
75
|
defined?(Knowledge)
|
|
72
76
|
end
|
|
73
77
|
|
|
@@ -81,6 +85,7 @@ module Legion
|
|
|
81
85
|
Transport::Messages::Query.new(payload).publish
|
|
82
86
|
{ success: true, dispatched: :transport, payload: payload }
|
|
83
87
|
rescue StandardError => e
|
|
88
|
+
handle_exception(e, level: :warn, operation: 'apollo.request.publish_query')
|
|
84
89
|
{ success: false, error: e.message }
|
|
85
90
|
end
|
|
86
91
|
|
|
@@ -88,6 +93,7 @@ module Legion
|
|
|
88
93
|
Transport::Messages::Ingest.new(payload).publish
|
|
89
94
|
{ success: true, dispatched: :transport, payload: payload }
|
|
90
95
|
rescue StandardError => e
|
|
96
|
+
handle_exception(e, level: :warn, operation: 'apollo.request.publish_ingest')
|
|
91
97
|
{ success: false, error: e.message }
|
|
92
98
|
end
|
|
93
99
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
require 'legion/settings'
|
|
5
|
+
require 'legion/json'
|
|
3
6
|
require 'legion/extensions/apollo/version'
|
|
4
7
|
require 'legion/extensions/apollo/helpers/confidence'
|
|
5
8
|
require 'legion/extensions/apollo/helpers/similarity'
|
|
@@ -7,6 +10,7 @@ require 'legion/extensions/apollo/helpers/graph_query'
|
|
|
7
10
|
require 'legion/extensions/apollo/helpers/tag_normalizer'
|
|
8
11
|
require 'legion/extensions/apollo/helpers/capability'
|
|
9
12
|
require 'legion/extensions/apollo/helpers/writeback'
|
|
13
|
+
require 'legion/extensions/apollo/helpers/data_models'
|
|
10
14
|
require 'legion/extensions/apollo/runners/knowledge'
|
|
11
15
|
require 'legion/extensions/apollo/runners/expertise'
|
|
12
16
|
require 'legion/extensions/apollo/runners/maintenance'
|
|
@@ -32,11 +36,103 @@ end
|
|
|
32
36
|
module Legion
|
|
33
37
|
module Extensions
|
|
34
38
|
module Apollo
|
|
39
|
+
extend Legion::Logging::Helper
|
|
40
|
+
extend Legion::Settings::Helper
|
|
35
41
|
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
|
|
36
42
|
|
|
37
43
|
def self.remote_invocable?
|
|
38
44
|
false
|
|
39
45
|
end
|
|
46
|
+
|
|
47
|
+
def self.default_settings # rubocop:disable Metrics/MethodLength
|
|
48
|
+
{
|
|
49
|
+
confidence: {
|
|
50
|
+
initial: 0.5,
|
|
51
|
+
corroboration_boost: 0.3,
|
|
52
|
+
retrieval_boost: 0.02,
|
|
53
|
+
write_gate: 0.6,
|
|
54
|
+
novelty_gate: 0.3,
|
|
55
|
+
corroboration_similarity: 0.9
|
|
56
|
+
},
|
|
57
|
+
power_law_alpha: 0.05,
|
|
58
|
+
decay_threshold: 0.05,
|
|
59
|
+
stale_days: 90,
|
|
60
|
+
decay_min_age_hours: 168,
|
|
61
|
+
graph: {
|
|
62
|
+
spread_factor: 0.6,
|
|
63
|
+
default_depth: 2,
|
|
64
|
+
min_activation: 0.1
|
|
65
|
+
},
|
|
66
|
+
query: {
|
|
67
|
+
default_limit: 10,
|
|
68
|
+
default_min_confidence: 0.3,
|
|
69
|
+
retrieval_limit: 5,
|
|
70
|
+
redistribute_min_confidence: 0.5,
|
|
71
|
+
mesh_export_min_confidence: 0.5,
|
|
72
|
+
mesh_export_limit: 100
|
|
73
|
+
},
|
|
74
|
+
gas: {
|
|
75
|
+
relate_confidence_gate: 0.7,
|
|
76
|
+
synthesis_confidence_cap: 0.7,
|
|
77
|
+
max_anticipations: 3,
|
|
78
|
+
similar_entries_limit: 3,
|
|
79
|
+
fallback_confidence: 0.5
|
|
80
|
+
},
|
|
81
|
+
maintenance: {
|
|
82
|
+
force_decay_factor: 0.5
|
|
83
|
+
},
|
|
84
|
+
contradiction: {
|
|
85
|
+
similar_limit: 10,
|
|
86
|
+
similarity_threshold: 0.7,
|
|
87
|
+
relation_weight: 0.8
|
|
88
|
+
},
|
|
89
|
+
corroboration: {
|
|
90
|
+
relation_weight: 1.0,
|
|
91
|
+
scan_limit: 50,
|
|
92
|
+
same_provider_weight: 0.5
|
|
93
|
+
},
|
|
94
|
+
expertise: {
|
|
95
|
+
initial_proficiency: 0.0,
|
|
96
|
+
min_agents_at_risk: 2,
|
|
97
|
+
proficiency_cap: 1.0
|
|
98
|
+
},
|
|
99
|
+
entity_extractor: {
|
|
100
|
+
min_confidence: 0.7
|
|
101
|
+
},
|
|
102
|
+
entity_watchdog: {
|
|
103
|
+
concept_keywords: [],
|
|
104
|
+
types: %w[person service repository concept],
|
|
105
|
+
detect_confidence: 0.5,
|
|
106
|
+
exists_min_confidence: 0.1,
|
|
107
|
+
min_confidence: 0.7,
|
|
108
|
+
dedup_threshold: 0.92,
|
|
109
|
+
lookback_seconds: 300,
|
|
110
|
+
log_limit: 50
|
|
111
|
+
},
|
|
112
|
+
domain_isolation: {
|
|
113
|
+
'claims_optimization' => ['claims_optimization'],
|
|
114
|
+
'clinical_care' => %w[clinical_care general],
|
|
115
|
+
'general' => :all
|
|
116
|
+
},
|
|
117
|
+
embedding: {
|
|
118
|
+
provider: nil,
|
|
119
|
+
model: nil
|
|
120
|
+
},
|
|
121
|
+
data: {
|
|
122
|
+
apollo_write: false
|
|
123
|
+
},
|
|
124
|
+
writeback: {
|
|
125
|
+
enabled: true,
|
|
126
|
+
min_content_length: 50
|
|
127
|
+
},
|
|
128
|
+
actors: {
|
|
129
|
+
decay_interval: 3600,
|
|
130
|
+
expertise_interval: 1800,
|
|
131
|
+
corroboration_interval: 900,
|
|
132
|
+
entity_watchdog_interval: 120
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
end
|
|
40
136
|
end
|
|
41
137
|
end
|
|
42
138
|
end
|
|
@@ -53,7 +53,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
|
|
|
53
53
|
{ vector: Array.new(1024, 0.1), model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 }
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
|
-
stub_const('Legion::LLM::Embeddings', embeddings_mod)
|
|
56
|
+
stub_const('Legion::LLM::Call::Embeddings', embeddings_mod)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
describe '#runner_function' do
|
|
@@ -66,7 +66,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
|
|
|
66
66
|
let(:payload) { { content: 'test content', content_type: 'observation', tags: %w[test] } }
|
|
67
67
|
|
|
68
68
|
before do
|
|
69
|
-
allow(Legion::LLM::Embeddings).to receive(:generate)
|
|
69
|
+
allow(Legion::LLM::Call::Embeddings).to receive(:generate)
|
|
70
70
|
.and_return({ vector: [0.1] * 1024, model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 })
|
|
71
71
|
allow(Legion::Extensions::Apollo::Helpers::Capability).to receive(:can_write?).and_return(false)
|
|
72
72
|
end
|
|
@@ -92,7 +92,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
it 'returns error hash on failure' do
|
|
95
|
-
allow(Legion::LLM::Embeddings).to receive(:generate).and_raise(RuntimeError, 'boom')
|
|
95
|
+
allow(Legion::LLM::Call::Embeddings).to receive(:generate).and_raise(RuntimeError, 'boom')
|
|
96
96
|
|
|
97
97
|
result = actor.handle_vectorize(payload)
|
|
98
98
|
expect(result[:success]).to be false
|