legion-apollo 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/legion/apollo/helpers/confidence.rb +1 -0
- data/lib/legion/apollo/local/graph.rb +1 -1
- data/lib/legion/apollo/local/migrations/004_add_versioning_tiers_inference.rb +53 -0
- data/lib/legion/apollo/local.rb +124 -24
- data/lib/legion/apollo/routes.rb +37 -22
- data/lib/legion/apollo/settings.rb +19 -1
- data/lib/legion/apollo/version.rb +1 -1
- data/lib/legion/apollo.rb +8 -3
- 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: 37c9cddf7269f0a38272eeedb3fe3f3abb50440271b4ec9686820307233fe12f
|
|
4
|
+
data.tar.gz: 28962732f0a157e4e6f5abbf2ad494cd4d9fdb56f9d3723de3d074710143118e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1a1999ff0d3731d67af120efc58c215faeccc6a278ba02c11bfbc84f45dfb50520556e7966bdc6ee9a29a7744cdd61ac3cbc31f99b5112a0c7c3a82e2011141
|
|
7
|
+
data.tar.gz: f4421e4ace8b3e735ae78012e8579a052ee054f2f1a72160cf401f47fcf74fef3ab96925d567378804fadcd84f290c774cdaf73ecc24d94f8fdf23427b9b76bf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Migration 004: versioning, tiers, inference, expiry metadata, and source linkage columns on `local_knowledge`; `local_source_links` table (#6-#11)
|
|
7
|
+
- Inference tagging: `is_inference` flag on ingest, `include_inferences:` filter on query, `INITIAL_INFERENCE_CONFIDENCE` (0.35) for LLM-derived entries (#9)
|
|
8
|
+
- Temporal expiry metadata: `forget_reason` and custom `expires_at` on ingest (#8)
|
|
9
|
+
- Versioned knowledge: `parent_knowledge_id`/`is_latest`/`supersession_type` on ingest, automatic parent supersession, `version_chain` traversal, `include_history:` query filter (#7)
|
|
10
|
+
- L0/L1/L2 tiered retrieval: `tier:` parameter on query with summary projection and truncation fallback (#6)
|
|
11
|
+
- Source-to-fact linkage: `source_uri`/`source_hash`/`relevance_score`/`extraction_method` on ingest, `source_links_for` query method, `local_source_links` table (#10)
|
|
12
|
+
- `SUPERSEDES` relation type in `Local::Graph` (#11)
|
|
13
|
+
- Versioning and expiry settings defaults
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- FTS5 search crashes on punctuation (`.`, `:`, `-`, `+`, etc.) by tokenizing input into quoted alphanumeric terms with implicit AND semantics; ILIKE fallback now escapes `%` and `_` wildcards (#22)
|
|
17
|
+
- Apollo query returns HTTP 500 on non-Postgres backends: `direct_query` exceptions normalized to `:backend_query_failed` symbol, `apollo_status_code` maps known unavailability symbols to 503 (#23)
|
|
18
|
+
|
|
3
19
|
## [0.4.0] - 2026-04-02
|
|
4
20
|
|
|
5
21
|
### Changed
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
# Relationships are directional typed edges between two entities.
|
|
12
12
|
# Graph traversal expands one frontier batch per depth to avoid per-node queries.
|
|
13
13
|
module Graph # rubocop:disable Metrics/ModuleLength
|
|
14
|
-
VALID_RELATION_TYPES = %w[AFFECTS OWNED_BY DEPENDS_ON RELATED_TO].freeze
|
|
14
|
+
VALID_RELATION_TYPES = %w[AFFECTS OWNED_BY DEPENDS_ON RELATED_TO SUPERSEDES].freeze
|
|
15
15
|
|
|
16
16
|
class << self # rubocop:disable Metrics/ClassLength
|
|
17
17
|
include Legion::Logging::Helper
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do # rubocop:disable Metrics/BlockLength
|
|
4
|
+
up do # rubocop:disable Metrics/BlockLength
|
|
5
|
+
alter_table(:local_knowledge) do
|
|
6
|
+
add_column :is_inference, :boolean, default: false, null: false
|
|
7
|
+
add_column :parent_knowledge_id, Integer, null: true
|
|
8
|
+
add_column :is_latest, :boolean, default: true, null: false
|
|
9
|
+
add_column :supersession_type, String, size: 32, null: true
|
|
10
|
+
add_column :forget_reason, String, size: 128, null: true
|
|
11
|
+
add_column :summary_l0, String, size: 500, null: true
|
|
12
|
+
add_column :summary_l1, :text, null: true
|
|
13
|
+
add_column :knowledge_tier, String, size: 4, null: false, default: 'L2'
|
|
14
|
+
add_column :l0_generated_at, String, null: true
|
|
15
|
+
add_column :l1_generated_at, String, null: true
|
|
16
|
+
|
|
17
|
+
add_index :is_latest, name: :idx_local_knowledge_is_latest
|
|
18
|
+
add_index :is_inference, name: :idx_local_knowledge_is_inference
|
|
19
|
+
add_index :knowledge_tier, name: :idx_local_knowledge_tier
|
|
20
|
+
add_index :parent_knowledge_id, name: :idx_local_knowledge_parent
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
create_table(:local_source_links) do
|
|
24
|
+
primary_key :id
|
|
25
|
+
Integer :entry_id, null: false
|
|
26
|
+
String :source_uri, text: true
|
|
27
|
+
String :source_hash, size: 64
|
|
28
|
+
Float :relevance_score, default: 1.0
|
|
29
|
+
String :extraction_method, size: 64
|
|
30
|
+
String :created_at, null: false
|
|
31
|
+
|
|
32
|
+
index :entry_id, name: :idx_source_links_entry
|
|
33
|
+
index :source_hash, name: :idx_source_links_hash
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
down do
|
|
38
|
+
drop_table(:local_source_links) if table_exists?(:local_source_links)
|
|
39
|
+
|
|
40
|
+
alter_table(:local_knowledge) do
|
|
41
|
+
drop_column :is_inference
|
|
42
|
+
drop_column :parent_knowledge_id
|
|
43
|
+
drop_column :is_latest
|
|
44
|
+
drop_column :supersession_type
|
|
45
|
+
drop_column :forget_reason
|
|
46
|
+
drop_column :summary_l0
|
|
47
|
+
drop_column :summary_l1
|
|
48
|
+
drop_column :knowledge_tier
|
|
49
|
+
drop_column :l0_generated_at
|
|
50
|
+
drop_column :l1_generated_at
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/legion/apollo/local.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'legion/logging'
|
|
|
5
5
|
require 'socket'
|
|
6
6
|
require 'time'
|
|
7
7
|
require_relative 'local/graph'
|
|
8
|
+
require_relative 'helpers/confidence'
|
|
8
9
|
require_relative 'helpers/similarity'
|
|
9
10
|
require_relative 'helpers/tag_normalizer'
|
|
10
11
|
|
|
@@ -90,7 +91,7 @@ module Legion
|
|
|
90
91
|
{ success: false, error: e.message }
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def query(text:, limit: nil, min_confidence: nil, tags: nil, **) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
94
|
+
def query(text:, limit: nil, min_confidence: nil, tags: nil, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
94
95
|
return not_started_error unless started?
|
|
95
96
|
|
|
96
97
|
text = normalize_text_input(text)
|
|
@@ -105,12 +106,19 @@ module Legion
|
|
|
105
106
|
log.debug { "Apollo::Local query limit=#{limit} min_confidence=#{min_confidence} tags=#{Array(tags).size}" }
|
|
106
107
|
|
|
107
108
|
candidates = fts_search(text, limit: limit * multiplier)
|
|
108
|
-
|
|
109
|
+
include_inferences = opts.fetch(:include_inferences, true)
|
|
110
|
+
include_history = opts.fetch(:include_history, false)
|
|
111
|
+
candidates = filter_candidates(candidates, min_confidence: min_confidence, tags: tags,
|
|
112
|
+
include_inferences: include_inferences,
|
|
113
|
+
include_history: include_history)
|
|
109
114
|
candidates = cosine_rerank(text, candidates) if can_rerank?
|
|
110
115
|
results = candidates.first(limit)
|
|
111
116
|
|
|
117
|
+
tier = opts[:tier]
|
|
118
|
+
results = results.map { |r| project_tier(r, tier) } if tier
|
|
119
|
+
|
|
112
120
|
log.info { "Apollo::Local query completed count=#{results.size}" }
|
|
113
|
-
{ success: true, results: results, count: results.size, mode: :local }
|
|
121
|
+
{ success: true, results: results, count: results.size, mode: :local, tier: tier }
|
|
114
122
|
rescue StandardError => e
|
|
115
123
|
handle_exception(
|
|
116
124
|
e,
|
|
@@ -223,6 +231,41 @@ module Legion
|
|
|
223
231
|
{ success: false, error: e.message }
|
|
224
232
|
end
|
|
225
233
|
|
|
234
|
+
def version_chain(entry_id:, max_depth: 50) # rubocop:disable Metrics/MethodLength
|
|
235
|
+
return not_started_error unless started?
|
|
236
|
+
|
|
237
|
+
chain = []
|
|
238
|
+
current_id = entry_id
|
|
239
|
+
seen = Set.new
|
|
240
|
+
|
|
241
|
+
max_depth.times do
|
|
242
|
+
break unless current_id
|
|
243
|
+
break if seen.include?(current_id)
|
|
244
|
+
|
|
245
|
+
seen.add(current_id)
|
|
246
|
+
row = db[:local_knowledge].where(id: current_id).first
|
|
247
|
+
break unless row
|
|
248
|
+
|
|
249
|
+
chain << row
|
|
250
|
+
current_id = row[:parent_knowledge_id]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
{ success: true, chain: chain, count: chain.size }
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
handle_exception(e, level: :error, operation: 'apollo.local.version_chain', entry_id: entry_id)
|
|
256
|
+
{ success: false, error: e.message }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def source_links_for(entry_id:)
|
|
260
|
+
return not_started_error unless started?
|
|
261
|
+
|
|
262
|
+
links = db[:local_source_links].where(entry_id: entry_id).all
|
|
263
|
+
{ success: true, links: links, count: links.size }
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
handle_exception(e, level: :error, operation: 'apollo.local.source_links_for', entry_id: entry_id)
|
|
266
|
+
{ success: false, error: e.message }
|
|
267
|
+
end
|
|
268
|
+
|
|
226
269
|
private
|
|
227
270
|
|
|
228
271
|
def self_knowledge_files
|
|
@@ -375,7 +418,8 @@ module Legion
|
|
|
375
418
|
log.debug { "Apollo::Local ingest hash=#{hash} tags=#{Array(tags).size} source_channel=#{opts[:source_channel]}" }
|
|
376
419
|
|
|
377
420
|
row = build_ingest_row(content: content, hash: hash, tags: tags, **opts)
|
|
378
|
-
id = persist_ingest_row(row)
|
|
421
|
+
id = persist_ingest_row(row, opts)
|
|
422
|
+
mark_parent_superseded(opts[:parent_knowledge_id]) if opts[:parent_knowledge_id]
|
|
379
423
|
|
|
380
424
|
log.info { "Apollo::Local ingest stored id=#{id} hash=#{hash}" }
|
|
381
425
|
{ success: true, mode: :local, id: id }
|
|
@@ -385,31 +429,60 @@ module Legion
|
|
|
385
429
|
deduplicated_ingest(hash)
|
|
386
430
|
end
|
|
387
431
|
|
|
388
|
-
def build_ingest_row(content:, hash:, tags:, **opts)
|
|
432
|
+
def build_ingest_row(content:, hash:, tags:, **opts) # rubocop:disable Metrics/MethodLength
|
|
433
|
+
is_inference = opts[:is_inference] == true
|
|
434
|
+
default_confidence = is_inference ? Legion::Apollo::Helpers::Confidence::INITIAL_INFERENCE_CONFIDENCE : 1.0
|
|
389
435
|
{
|
|
390
|
-
content:
|
|
391
|
-
content_hash:
|
|
392
|
-
tags:
|
|
393
|
-
source_channel:
|
|
394
|
-
source_agent:
|
|
395
|
-
submitted_by:
|
|
396
|
-
confidence:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
436
|
+
content: content,
|
|
437
|
+
content_hash: hash,
|
|
438
|
+
tags: serialized_tags(tags),
|
|
439
|
+
source_channel: opts[:source_channel],
|
|
440
|
+
source_agent: opts[:source_agent],
|
|
441
|
+
submitted_by: opts[:submitted_by],
|
|
442
|
+
confidence: opts[:confidence] || default_confidence,
|
|
443
|
+
is_inference: is_inference,
|
|
444
|
+
forget_reason: opts[:forget_reason],
|
|
445
|
+
parent_knowledge_id: opts[:parent_knowledge_id],
|
|
446
|
+
supersession_type: opts[:supersession_type]
|
|
447
|
+
}.merge(embedding_columns(content, opts)).merge(timestamp_columns)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def persist_ingest_row(row, opts = {})
|
|
401
451
|
db.transaction do
|
|
402
452
|
id = db[:local_knowledge].insert(row)
|
|
403
453
|
sync_fts!(id, row[:content], row[:tags])
|
|
454
|
+
create_source_link(id, opts) if opts[:source_uri]
|
|
404
455
|
id
|
|
405
456
|
end
|
|
406
457
|
end
|
|
407
458
|
|
|
459
|
+
def create_source_link(entry_id, opts)
|
|
460
|
+
db[:local_source_links].insert(
|
|
461
|
+
entry_id: entry_id,
|
|
462
|
+
source_uri: opts[:source_uri],
|
|
463
|
+
source_hash: opts[:source_hash],
|
|
464
|
+
relevance_score: opts[:relevance_score] || 1.0,
|
|
465
|
+
extraction_method: opts[:extraction_method],
|
|
466
|
+
created_at: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
467
|
+
)
|
|
468
|
+
rescue StandardError => e
|
|
469
|
+
handle_exception(e, level: :warn, operation: 'apollo.local.create_source_link', entry_id: entry_id)
|
|
470
|
+
end
|
|
471
|
+
|
|
408
472
|
def deduplicated_ingest(hash)
|
|
409
473
|
log.info { "Apollo::Local ingest deduplicated hash=#{hash}" }
|
|
410
474
|
{ success: true, mode: :deduplicated }
|
|
411
475
|
end
|
|
412
476
|
|
|
477
|
+
def mark_parent_superseded(parent_id)
|
|
478
|
+
return unless parent_id
|
|
479
|
+
|
|
480
|
+
db[:local_knowledge].where(id: parent_id).update(is_latest: false)
|
|
481
|
+
log.info { "Apollo::Local marked entry id=#{parent_id} as superseded" }
|
|
482
|
+
rescue StandardError => e
|
|
483
|
+
handle_exception(e, level: :warn, operation: 'apollo.local.mark_parent_superseded', parent_id: parent_id)
|
|
484
|
+
end
|
|
485
|
+
|
|
413
486
|
def generate_embedding(content) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
414
487
|
unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:can_embed?) && Legion::LLM.can_embed?
|
|
415
488
|
log.debug 'Apollo::Local embedding skipped because embeddings are unavailable'
|
|
@@ -447,13 +520,13 @@ module Legion
|
|
|
447
520
|
log.debug { "Apollo::Local FTS synced id=#{id}" }
|
|
448
521
|
end
|
|
449
522
|
|
|
450
|
-
def embedding_columns(content)
|
|
523
|
+
def embedding_columns(content, opts = {})
|
|
451
524
|
embedding, embedded_at = generate_embedding(content)
|
|
452
525
|
|
|
453
526
|
{
|
|
454
527
|
embedding: embedding ? Legion::JSON.dump(embedding) : nil,
|
|
455
528
|
embedded_at: embedded_at,
|
|
456
|
-
expires_at: compute_expires_at
|
|
529
|
+
expires_at: opts[:expires_at] || compute_expires_at
|
|
457
530
|
}
|
|
458
531
|
end
|
|
459
532
|
|
|
@@ -467,15 +540,18 @@ module Legion
|
|
|
467
540
|
end
|
|
468
541
|
|
|
469
542
|
def fts_search(text, limit:) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
543
|
+
now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
470
544
|
if text.to_s.strip.empty?
|
|
471
545
|
return db[:local_knowledge]
|
|
472
|
-
.where(Sequel.lit('expires_at > ?',
|
|
546
|
+
.where(Sequel.lit('expires_at > ?', now))
|
|
473
547
|
.limit(limit)
|
|
474
548
|
.all
|
|
475
549
|
end
|
|
476
550
|
|
|
477
|
-
|
|
478
|
-
now
|
|
551
|
+
tokens = text.to_s.scan(/[\p{L}\p{N}_]+/)
|
|
552
|
+
return ilike_search(text, now: now, limit: limit) if tokens.empty?
|
|
553
|
+
|
|
554
|
+
escaped = tokens.map { |t| %("#{t}") }.join(' ')
|
|
479
555
|
db.fetch(
|
|
480
556
|
'SELECT lk.* FROM local_knowledge lk ' \
|
|
481
557
|
'INNER JOIN local_knowledge_fts fts ON lk.id = fts.rowid ' \
|
|
@@ -484,15 +560,24 @@ module Legion
|
|
|
484
560
|
).all
|
|
485
561
|
rescue StandardError => e
|
|
486
562
|
handle_exception(e, level: :debug, operation: 'apollo.local.fts_search', limit: limit, fallback: :ilike)
|
|
563
|
+
ilike_search(text, now: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), limit: limit)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def ilike_search(text, now:, limit:)
|
|
567
|
+
safe_text = text.to_s.gsub('\\', '\\\\\\\\').gsub('%', '\%').gsub('_', '\_')
|
|
487
568
|
db[:local_knowledge]
|
|
488
|
-
.where(Sequel.lit('expires_at > ?',
|
|
489
|
-
.where(Sequel.
|
|
569
|
+
.where(Sequel.lit('expires_at > ?', now))
|
|
570
|
+
.where(Sequel.lit("content LIKE ? ESCAPE '\\' COLLATE NOCASE", "%#{safe_text}%"))
|
|
490
571
|
.limit(limit)
|
|
491
572
|
.all
|
|
492
573
|
end
|
|
493
574
|
|
|
494
|
-
def filter_candidates(candidates, min_confidence:, tags:)
|
|
575
|
+
def filter_candidates(candidates, min_confidence:, tags:, include_inferences: true, include_history: false) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize
|
|
495
576
|
candidates = candidates.select { |c| (c[:confidence] || 0) >= min_confidence }
|
|
577
|
+
candidates = candidates.reject { |c| [1, true].include?(c[:is_inference]) } unless include_inferences
|
|
578
|
+
unless include_history
|
|
579
|
+
candidates = candidates.select { |c| c[:is_latest].nil? || c[:is_latest] == 1 || c[:is_latest] == true }
|
|
580
|
+
end
|
|
496
581
|
if tags && !tags.empty?
|
|
497
582
|
tag_set = Array(tags).map(&:to_s)
|
|
498
583
|
candidates = candidates.select do |c|
|
|
@@ -658,6 +743,21 @@ module Legion
|
|
|
658
743
|
log.debug { "Apollo::Local FTS rebuilt id=#{id}" }
|
|
659
744
|
end
|
|
660
745
|
|
|
746
|
+
def project_tier(entry, tier) # rubocop:disable Metrics/MethodLength
|
|
747
|
+
case tier
|
|
748
|
+
when :l0
|
|
749
|
+
entry.slice(:id, :content_hash, :confidence, :tags, :source_channel, :is_inference, :is_latest).merge(
|
|
750
|
+
summary: entry[:summary_l0] || entry[:content]&.slice(0, 200)
|
|
751
|
+
)
|
|
752
|
+
when :l1
|
|
753
|
+
entry.slice(:id, :content_hash, :confidence, :tags, :source_channel, :is_inference, :is_latest).merge(
|
|
754
|
+
summary: entry[:summary_l1] || entry[:content]&.slice(0, 1000)
|
|
755
|
+
)
|
|
756
|
+
else
|
|
757
|
+
entry
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
661
761
|
def not_started_error
|
|
662
762
|
{ success: false, error: :not_started }
|
|
663
763
|
end
|
data/lib/legion/apollo/routes.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Legion
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def self.register_query_route(app) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
53
|
+
def self.register_query_route(app) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
54
54
|
app.post '/api/apollo/query' do
|
|
55
55
|
unless apollo_api_available?
|
|
56
56
|
halt 503, json_error('apollo_unavailable', 'apollo is not available', status_code: 503)
|
|
@@ -59,21 +59,24 @@ module Legion
|
|
|
59
59
|
body = parse_request_body
|
|
60
60
|
default_limit = defined?(Legion::Settings) ? (Legion::Settings[:apollo]&.dig(:default_limit) || 5) : 5
|
|
61
61
|
result = Legion::Apollo.query(
|
|
62
|
-
text:
|
|
63
|
-
limit:
|
|
64
|
-
min_confidence:
|
|
65
|
-
status:
|
|
66
|
-
tags:
|
|
67
|
-
domain:
|
|
68
|
-
agent_id:
|
|
69
|
-
scope:
|
|
62
|
+
text: body[:query],
|
|
63
|
+
limit: body[:limit] || default_limit,
|
|
64
|
+
min_confidence: body[:min_confidence],
|
|
65
|
+
status: body[:status] || [:confirmed],
|
|
66
|
+
tags: body[:tags],
|
|
67
|
+
domain: body[:domain],
|
|
68
|
+
agent_id: body[:agent_id] || 'api',
|
|
69
|
+
scope: normalize_scope(body[:scope]),
|
|
70
|
+
tier: body[:tier]&.to_sym,
|
|
71
|
+
include_inferences: body.fetch(:include_inferences, true),
|
|
72
|
+
include_history: body.fetch(:include_history, false)
|
|
70
73
|
)
|
|
71
74
|
json_response(result, status_code: apollo_status_code(result))
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def self.register_ingest_route(app) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
76
|
-
app.post '/api/apollo/ingest' do
|
|
79
|
+
app.post '/api/apollo/ingest' do # rubocop:disable Metrics/BlockLength
|
|
77
80
|
unless apollo_api_available?
|
|
78
81
|
halt 503, json_error('apollo_unavailable', 'apollo is not available', status_code: 503)
|
|
79
82
|
end
|
|
@@ -84,15 +87,24 @@ module Legion
|
|
|
84
87
|
effective_max_tags = [max_tags, Legion::Apollo::Helpers::TagNormalizer::MAX_TAGS].min
|
|
85
88
|
tags = Legion::Apollo::Helpers::TagNormalizer.normalize(Array(body[:tags])).first(effective_max_tags)
|
|
86
89
|
result = Legion::Apollo.ingest(
|
|
87
|
-
content:
|
|
88
|
-
content_type:
|
|
89
|
-
tags:
|
|
90
|
-
source_agent:
|
|
91
|
-
source_provider:
|
|
92
|
-
source_channel:
|
|
93
|
-
knowledge_domain:
|
|
94
|
-
context:
|
|
95
|
-
scope:
|
|
90
|
+
content: body[:content],
|
|
91
|
+
content_type: body[:content_type] || :observation,
|
|
92
|
+
tags: tags,
|
|
93
|
+
source_agent: body[:source_agent] || 'api',
|
|
94
|
+
source_provider: body[:source_provider],
|
|
95
|
+
source_channel: body[:source_channel] || 'rest_api',
|
|
96
|
+
knowledge_domain: body[:knowledge_domain],
|
|
97
|
+
context: body[:context] || {},
|
|
98
|
+
scope: normalize_scope(body[:scope]),
|
|
99
|
+
is_inference: body[:is_inference] == true,
|
|
100
|
+
forget_reason: body[:forget_reason],
|
|
101
|
+
expires_at: body[:expires_at],
|
|
102
|
+
parent_knowledge_id: body[:parent_knowledge_id],
|
|
103
|
+
supersession_type: body[:supersession_type],
|
|
104
|
+
source_uri: body[:source_uri],
|
|
105
|
+
source_hash: body[:source_hash],
|
|
106
|
+
relevance_score: body[:relevance_score],
|
|
107
|
+
extraction_method: body[:extraction_method]
|
|
96
108
|
)
|
|
97
109
|
json_response(result, status_code: apollo_status_code(result, success_status: 201))
|
|
98
110
|
end
|
|
@@ -197,13 +209,16 @@ module Legion
|
|
|
197
209
|
:global
|
|
198
210
|
end
|
|
199
211
|
|
|
200
|
-
def apollo_status_code(result, success_status: 200)
|
|
212
|
+
def apollo_status_code(result, success_status: 200) # rubocop:disable Metrics/MethodLength
|
|
201
213
|
return 202 if result[:success] && result[:mode] == :async
|
|
202
214
|
return success_status if result[:success]
|
|
203
215
|
|
|
204
216
|
case result[:error]
|
|
205
|
-
when :no_path_available, :not_started
|
|
206
|
-
|
|
217
|
+
when :no_path_available, :not_started, :local_not_started,
|
|
218
|
+
:upstream_query_failed, :backend_query_failed
|
|
219
|
+
503
|
|
220
|
+
else
|
|
221
|
+
500
|
|
207
222
|
end
|
|
208
223
|
rescue StandardError => e
|
|
209
224
|
handle_exception(e, level: :debug, operation: :apollo_status_code)
|
|
@@ -10,7 +10,9 @@ module Legion
|
|
|
10
10
|
max_tags: 20,
|
|
11
11
|
default_limit: 5,
|
|
12
12
|
min_confidence: 0.3,
|
|
13
|
-
local: local_defaults
|
|
13
|
+
local: local_defaults,
|
|
14
|
+
versioning: versioning_defaults,
|
|
15
|
+
expiry: expiry_defaults
|
|
14
16
|
}
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -24,6 +26,22 @@ module Legion
|
|
|
24
26
|
default_limit: 5
|
|
25
27
|
}
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
def self.versioning_defaults
|
|
31
|
+
{
|
|
32
|
+
enabled: true,
|
|
33
|
+
supersession_threshold: 0.85,
|
|
34
|
+
max_chain_depth: 50
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.expiry_defaults
|
|
39
|
+
{
|
|
40
|
+
enabled: true,
|
|
41
|
+
sweep_interval: 3600,
|
|
42
|
+
warn_before_expiry: 86_400
|
|
43
|
+
}
|
|
44
|
+
end
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
end
|
data/lib/legion/apollo.rb
CHANGED
|
@@ -247,7 +247,7 @@ module Legion
|
|
|
247
247
|
Legion::Extensions::Apollo::Runners::Knowledge.handle_query(**normalize_query_payload(payload))
|
|
248
248
|
rescue StandardError => e
|
|
249
249
|
handle_exception(e, level: :error, operation: 'apollo.direct_query', payload_keys: payload.keys)
|
|
250
|
-
{ success: false, error: e.message }
|
|
250
|
+
{ success: false, error: :backend_query_failed, detail: e.message }
|
|
251
251
|
end
|
|
252
252
|
|
|
253
253
|
def direct_ingest(payload)
|
|
@@ -288,7 +288,8 @@ module Legion
|
|
|
288
288
|
"Apollo query using local store text_length=#{payload[:text].to_s.length} " \
|
|
289
289
|
"limit=#{payload[:limit]}"
|
|
290
290
|
end
|
|
291
|
-
result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags
|
|
291
|
+
result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
|
|
292
|
+
:tier, :include_inferences, :include_history))
|
|
292
293
|
return result unless result[:success]
|
|
293
294
|
|
|
294
295
|
entries = normalize_local_entries(Array(result[:results]))
|
|
@@ -322,7 +323,8 @@ module Legion
|
|
|
322
323
|
|
|
323
324
|
if Legion::Apollo::Local.started?
|
|
324
325
|
attempted = true
|
|
325
|
-
local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags
|
|
326
|
+
local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
|
|
327
|
+
:tier, :include_inferences, :include_history))
|
|
326
328
|
if local[:success]
|
|
327
329
|
any_success = true
|
|
328
330
|
entries.concat(normalize_local_entries(Array(local[:results]))) if local[:results]
|
|
@@ -341,6 +343,9 @@ module Legion
|
|
|
341
343
|
return { success: false, error: :no_path_available } unless attempted
|
|
342
344
|
|
|
343
345
|
unless any_success
|
|
346
|
+
symbol_errors = errors.compact.grep(Symbol).uniq
|
|
347
|
+
return { success: false, error: symbol_errors.first } if symbol_errors.size == 1
|
|
348
|
+
|
|
344
349
|
combined_error = errors.compact.map(&:to_s).reject(&:empty?).join('; ')
|
|
345
350
|
combined_error = :upstream_query_failed if combined_error.empty?
|
|
346
351
|
return { success: false, error: combined_error }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-apollo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- lib/legion/apollo/local/migrations/001_create_local_knowledge.rb
|
|
87
87
|
- lib/legion/apollo/local/migrations/002_create_graph_tables.rb
|
|
88
88
|
- lib/legion/apollo/local/migrations/003_harden_graph_relationships.rb
|
|
89
|
+
- lib/legion/apollo/local/migrations/004_add_versioning_tiers_inference.rb
|
|
89
90
|
- lib/legion/apollo/messages/access_boost.rb
|
|
90
91
|
- lib/legion/apollo/messages/ingest.rb
|
|
91
92
|
- lib/legion/apollo/messages/query.rb
|