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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7468f022e582731ace40da083285569d57c98e38ef375e5745f43d93e9c9d171
4
- data.tar.gz: 3d2bb59a17a6060e690c7321fc914d2dc2e2bdd7bb1ec108917bf86fc7f26c72
3
+ metadata.gz: 37c9cddf7269f0a38272eeedb3fe3f3abb50440271b4ec9686820307233fe12f
4
+ data.tar.gz: 28962732f0a157e4e6f5abbf2ad494cd4d9fdb56f9d3723de3d074710143118e
5
5
  SHA512:
6
- metadata.gz: a47cb6e18c47432ec0f3a15872f52dc193d7253fda94833da1c63d6046ad39bba8894f10dacc1c3472d7ef538157aad39cf1cf9894061623a9f1a162f69a5c16
7
- data.tar.gz: 3ca7dc35c483089a4b2c6bb0af57b524afe3a3cce571b893a14a4aa01f74fbb01b51b5eb09040389db09a6b5f2516d0bd6d97fc499bf8c912aadc6c3b622a587
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
@@ -17,6 +17,7 @@ module Legion
17
17
  WRITE_GATE_THRESHOLD = 0.3
18
18
  HIGH_CONFIDENCE = 0.8
19
19
  ARCHIVE_THRESHOLD = 0.1
20
+ INITIAL_INFERENCE_CONFIDENCE = 0.35
20
21
 
21
22
  STATUSES = %i[pending confirmed disputed deprecated archived].freeze
22
23
 
@@ -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
@@ -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
- candidates = filter_candidates(candidates, min_confidence: min_confidence, tags: tags)
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: content,
391
- content_hash: hash,
392
- tags: serialized_tags(tags),
393
- source_channel: opts[:source_channel],
394
- source_agent: opts[:source_agent],
395
- submitted_by: opts[:submitted_by],
396
- confidence: opts[:confidence] || 1.0
397
- }.merge(embedding_columns(content)).merge(timestamp_columns)
398
- end
399
-
400
- def persist_ingest_row(row)
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 > ?', Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')))
546
+ .where(Sequel.lit('expires_at > ?', now))
473
547
  .limit(limit)
474
548
  .all
475
549
  end
476
550
 
477
- escaped = text.to_s.gsub('"', '""')
478
- now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
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 > ?', Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')))
489
- .where(Sequel.ilike(:content, "%#{text}%"))
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
@@ -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: 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])
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: body[:content],
88
- content_type: body[:content_type] || :observation,
89
- tags: tags,
90
- source_agent: body[:source_agent] || 'api',
91
- source_provider: body[:source_provider],
92
- source_channel: body[:source_channel] || 'rest_api',
93
- knowledge_domain: body[:knowledge_domain],
94
- context: body[:context] || {},
95
- scope: normalize_scope(body[: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 then 503
206
- else 500
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Apollo
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  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.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