legion-apollo 0.5.6 → 0.5.13

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: 7150a2def38a098c16a2f35362210994cc77537ca6ee3e0e615823cf8e1797c2
4
- data.tar.gz: 6e0bfa593f70915134195672dbe321f03c00c86e1f80fee06f5370f3c6578110
3
+ metadata.gz: f096b7552b38d67434c6ae3431526bcd37ebe43c75fa90a19c8ec790e3d323a0
4
+ data.tar.gz: c8484053dfcda88edee17da060f7a5cab87235c0ea6258d2faecc6542135b1f5
5
5
  SHA512:
6
- metadata.gz: 97783044f23209d697578c0f56d570bc48b9531e5d569f23e4db98bf55cb5d0c4602d6a0fedeb8e7c04a89618d4586c25bc7ca89f1a8d922e8618c5b724cc13e
7
- data.tar.gz: 8498fae869abc9f54520b216ed88e2aea006ee6e520d8421d44cf5781c76ae9c158553e3c34a7e9e1e37d44594248a5c6daae1c7b55336c9692e7b668f00fbb4
6
+ metadata.gz: 246787437bfdf6c9667563e73bbba1bd56fe3328f2d1836d2a9daf14cc870ad1df25f96ff725f80a14ed9e4174efa4479f0c41eb314538138122d54cb8dbb424
7
+ data.tar.gz: fcee5919bc41cc594d77ab04188212265c54b5281383c9ff6e003222ea4ce2cf25859b39666cdf16c2b09fbcabbfd998af84c33d6fad4cdca86b683e1c140739
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.13] - 2026-10-29
4
+
5
+ ### Fixed
6
+ - Preserve L0/L1 summaries when normalizing public local and merged retrieval results, exposing the projected text as content instead of nil.
7
+
8
+ ## [0.5.12] - 2026-10-29
9
+
10
+ ### Fixed
11
+ - Honor raw source and temporal-window updates during upsert. Refresh derived raw content when indexed content changes, preserve omitted explicit sources/windows, and allow explicit nil windows to clear validity bounds.
12
+ - Remove old FTS5 terms before changing their external content row, preventing index corruption during upsert while preserving transactional rollback.
13
+
14
+ ## [0.5.11] - 2026-10-29
15
+
16
+ ### Fixed
17
+ - Renew expired content on duplicate ingestion while preserving its ID, source, tags, and deduplicated response; active duplicates retain their original expiration.
18
+
19
+ ## [0.5.10] - 2026-10-29
20
+
21
+ ### Fixed
22
+ - Apply local eligibility filters before filling the candidate budget, streaming full-text, blank, and fallback searches so rejected rows cannot hide later matching knowledge.
23
+
24
+ ## [0.5.9] - 2026-10-29
25
+
26
+ ### Fixed
27
+ - Return logged failure results from local graph operations when the optional database is absent, disconnected, or has no usable connection, instead of leaking lifecycle exceptions.
28
+
29
+ ## [0.5.8] - 2026-10-29
30
+
31
+ ### Fixed
32
+ - Generate local ingest/upsert embeddings outside the write mutex so slow providers do not serialize unrelated writes; retain atomic duplicate checks and persistence.
33
+
34
+ ## [0.5.7] - 2026-07-15
35
+ ### Removed
36
+ - Remove .md self-knowledge seed pipeline (bonds must be earned, not implanted)
37
+
3
38
  ## [0.5.5] - 2026-05-15
4
39
 
5
40
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Apollo is the LegionIO knowledge client. It gives extensions one API for writing, retrieving, and merging knowledge across the global Apollo service and the node-local SQLite store.
4
4
 
5
- **Version**: 0.5.2
5
+ **Version**: 0.5.13
6
6
 
7
7
  `legion-apollo` provides `query`, `ingest`, and `retrieve` with smart routing: co-located `lex-apollo`, RabbitMQ transport, node-local SQLite, or graceful failure. `Apollo::Local` mirrors the same public API for offline and low-latency retrieval without requiring remote infrastructure.
8
8
 
@@ -58,10 +58,18 @@ Features:
58
58
  - `raw_content` preservation for verbatim source text
59
59
  - `valid_from` / `valid_to` temporal windows with `as_of:` query filtering
60
60
  - Optional LLM embeddings (1024-dim) with cosine rerank when `Legion::LLM.can_embed?`
61
- - TTL expiry (default 5-year retention)
61
+ - Embedding generation runs outside the local write mutex; persistence and duplicate checks remain serialized
62
+ - TTL expiry (default 5-year retention); re-ingesting expired content renews retention without replacing its stored source or ID
62
63
  - FTS5 full-text search with `ILIKE` fallback
64
+ - Confidence, tags, visibility, inference, and history filters run before filling the reranking candidate budget
63
65
  - Null-byte removal and invalid UTF-8 scrubbing before persistence or backend routing
64
66
 
67
+ Local graph operations return `{ success: false, error: ... }` when the optional database is unavailable; they do not require callers to rescue missing-connection exceptions.
68
+
69
+ `Local.upsert` honors explicit `raw_content`, `valid_from`, and `valid_to` updates. Omitted validity bounds and distinct verbatim sources are preserved; derived raw content follows the updated indexed content. Explicit nil validity bounds clear the corresponding restriction.
70
+
71
+ Public `query`/`retrieve` calls with `tier: :l0` or `tier: :l1` retain the local `summary` and expose it as `content` in both `scope: :local` and `scope: :all` results.
72
+
65
73
  ## Configuration
66
74
 
67
75
  ```json
@@ -12,6 +12,7 @@ module Legion
12
12
  # Graph traversal expands one frontier batch per depth to avoid per-node queries.
13
13
  module Graph # rubocop:disable Metrics/ModuleLength
14
14
  VALID_RELATION_TYPES = %w[AFFECTS OWNED_BY DEPENDS_ON RELATED_TO SUPERSEDES].freeze
15
+ class DatabaseUnavailable < StandardError; end
15
16
 
16
17
  class << self # rubocop:disable Metrics/ClassLength
17
18
  include Legion::Logging::Helper
@@ -32,7 +33,7 @@ module Legion
32
33
  end
33
34
  log.info { "Apollo::Local::Graph created entity id=#{id} type=#{type} name=#{name}" }
34
35
  { success: true, id: id }
35
- rescue Sequel::Error => e
36
+ rescue DatabaseUnavailable, Sequel::Error => e
36
37
  handle_exception(
37
38
  e,
38
39
  level: :error,
@@ -49,7 +50,7 @@ module Legion
49
50
 
50
51
  log.debug { "Apollo::Local::Graph found entity id=#{id}" }
51
52
  { success: true, entity: decode_entity(row) }
52
- rescue Sequel::Error => e
53
+ rescue DatabaseUnavailable, Sequel::Error => e
53
54
  handle_exception(e, level: :error, operation: 'apollo.local.graph.find_entity', entity_id: id)
54
55
  { success: false, error: e.message }
55
56
  end
@@ -58,7 +59,7 @@ module Legion
58
59
  rows = db[:local_entities].where(entity_type: type.to_s).limit(limit).all
59
60
  log.debug { "Apollo::Local::Graph found entities type=#{type} count=#{rows.size}" }
60
61
  { success: true, entities: rows.map { |r| decode_entity(r) }, count: rows.size }
61
- rescue Sequel::Error => e
62
+ rescue DatabaseUnavailable, Sequel::Error => e
62
63
  handle_exception(
63
64
  e,
64
65
  level: :error,
@@ -73,7 +74,7 @@ module Legion
73
74
  rows = db[:local_entities].where(name: name.to_s).limit(limit).all
74
75
  log.debug { "Apollo::Local::Graph found entities name=#{name} count=#{rows.size}" }
75
76
  { success: true, entities: rows.map { |r| decode_entity(r) }, count: rows.size }
76
- rescue Sequel::Error => e
77
+ rescue DatabaseUnavailable, Sequel::Error => e
77
78
  handle_exception(
78
79
  e,
79
80
  level: :error,
@@ -95,7 +96,7 @@ module Legion
95
96
 
96
97
  log.info { "Apollo::Local::Graph updated entity id=#{id}" }
97
98
  { success: true, id: id }
98
- rescue Sequel::Error => e
99
+ rescue DatabaseUnavailable, Sequel::Error => e
99
100
  handle_exception(
100
101
  e,
101
102
  level: :error,
@@ -112,7 +113,7 @@ module Legion
112
113
 
113
114
  log.info { "Apollo::Local::Graph deleted entity id=#{id}" }
114
115
  result
115
- rescue Sequel::Error => e
116
+ rescue DatabaseUnavailable, Sequel::Error => e
116
117
  handle_exception(e, level: :error, operation: 'apollo.local.graph.delete_entity', entity_id: id)
117
118
  { success: false, error: e.message }
118
119
  end
@@ -132,6 +133,10 @@ module Legion
132
133
  "target_id=#{target_id} relation_type=#{normalized_relation_type}"
133
134
  end
134
135
  { success: true, id: id }
136
+ rescue DatabaseUnavailable => e
137
+ handle_exception(e, level: :error, operation: 'apollo.local.graph.create_relationship',
138
+ source_id: source_id, target_id: target_id)
139
+ { success: false, error: e.message }
135
140
  rescue Sequel::UniqueConstraintViolation => e
136
141
  duplicate = handle_duplicate_relationship(source_id, target_id, normalized_relation_type)
137
142
  return duplicate if duplicate
@@ -170,7 +175,7 @@ module Legion
170
175
  "relation_type=#{relation_type || 'any'} count=#{rows.size}"
171
176
  end
172
177
  { success: true, relationships: rows.map { |r| decode_relationship(r) }, count: rows.size }
173
- rescue Sequel::Error => e
178
+ rescue DatabaseUnavailable, Sequel::Error => e
174
179
  handle_exception(
175
180
  e,
176
181
  level: :error,
@@ -188,7 +193,7 @@ module Legion
188
193
 
189
194
  log.info { "Apollo::Local::Graph deleted relationship id=#{id}" }
190
195
  { success: true, id: id }
191
- rescue Sequel::Error => e
196
+ rescue DatabaseUnavailable, Sequel::Error => e
192
197
  handle_exception(e, level: :error, operation: 'apollo.local.graph.delete_relationship', relationship_id: id)
193
198
  { success: false, error: e.message }
194
199
  end
@@ -217,7 +222,7 @@ module Legion
217
222
  edges: edge_rows.map { |r| decode_relationship(r) },
218
223
  count: entity_rows.size
219
224
  }
220
- rescue Sequel::Error => e
225
+ rescue DatabaseUnavailable, Sequel::Error => e
221
226
  handle_exception(
222
227
  e,
223
228
  level: :error,
@@ -357,7 +362,14 @@ module Legion
357
362
  end
358
363
 
359
364
  def db
360
- Legion::Data::Local.connection
365
+ unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
366
+ raise DatabaseUnavailable, 'local database unavailable'
367
+ end
368
+
369
+ connection = Legion::Data::Local.connection
370
+ return connection if connection && !(connection.respond_to?(:closed?) && connection.closed?)
371
+
372
+ raise DatabaseUnavailable, 'local database unavailable'
361
373
  end
362
374
 
363
375
  def timestamp
@@ -17,8 +17,6 @@ module Legion
17
17
  MIGRATION_PATH = File.expand_path('local/migrations', __dir__).freeze
18
18
  LIFECYCLE_MUTEX = Mutex.new
19
19
  WRITE_MUTEX = Mutex.new
20
- SEED_MUTEX = Mutex.new
21
- HYDRATION_MUTEX = Mutex.new
22
20
 
23
21
  class << self # rubocop:disable Metrics/ClassLength
24
22
  include Legion::Logging::Helper
@@ -33,25 +31,29 @@ module Legion
33
31
  def shutdown
34
32
  LIFECYCLE_MUTEX.synchronize do
35
33
  @started = false
36
- @seeded = false
37
34
  log.info 'Legion::Apollo::Local shutdown'
38
35
  end
39
36
  rescue StandardError => e
40
37
  handle_exception(e, level: :warn, operation: 'apollo.local.shutdown')
41
38
  @started = false
42
- @seeded = false
43
39
  end
44
40
 
45
41
  def started?
46
42
  @started == true
47
43
  end
48
44
 
49
- def ingest(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength
45
+ def ingest(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
50
46
  return not_started_error unless started?
51
47
 
52
48
  tags = normalize_tags_input(tags)
49
+ content = normalize_text_input(content)
50
+ hash = content_hash(content)
51
+ duplicate = WRITE_MUTEX.synchronize { deduplicated_ingest(hash, expires_at: opts[:expires_at]) }
52
+ return duplicate if duplicate
53
+
54
+ embedding_fields = embedding_columns(content, opts)
53
55
  WRITE_MUTEX.synchronize do
54
- ingest_without_lock(content: content, tags: tags,
56
+ ingest_without_lock(content: content, tags: tags, embedding_fields: embedding_fields,
55
57
  **inject_identity_context(opts).merge(access_scope: access_scope))
56
58
  end
57
59
  rescue StandardError => e
@@ -71,14 +73,26 @@ module Legion
71
73
  sorted_tags = normalize_tags_input(tags).sort
72
74
  tag_json = Legion::JSON.dump(sorted_tags)
73
75
  merged_opts = inject_identity_context(opts).merge(access_scope: access_scope)
76
+ content = normalize_text_input(content)
77
+ hash = content_hash(content)
78
+ duplicate = WRITE_MUTEX.synchronize do
79
+ if db[:local_knowledge].where(tags: tag_json).empty?
80
+ deduplicated_ingest(hash,
81
+ expires_at: opts[:expires_at])
82
+ end
83
+ end
84
+ return duplicate if duplicate
85
+
86
+ embedding_fields = embedding_columns(content, merged_opts)
74
87
  WRITE_MUTEX.synchronize do
75
88
  existing = db[:local_knowledge].where(tags: tag_json).first
76
89
 
77
90
  if existing
78
- update_upsert_entry(existing, content, tag_json, merged_opts)
91
+ update_upsert_entry(existing, content, tag_json, merged_opts, embedding_fields)
79
92
  else
80
- result = ingest_without_lock(content: content, tags: sorted_tags, **merged_opts)
81
- result[:mode] = :inserted if result[:success] && result[:mode] != :deduplicated
93
+ result = ingest_without_lock(content: content, tags: sorted_tags,
94
+ embedding_fields: embedding_fields, **merged_opts)
95
+ result[:mode] = :inserted if result[:mode] == :local
82
96
  result
83
97
  end
84
98
  end
@@ -108,13 +122,15 @@ module Legion
108
122
  end
109
123
  log.debug { "Apollo::Local query limit=#{limit} min_confidence=#{min_confidence} tags=#{Array(tags).size}" }
110
124
 
111
- candidates = fts_search(text, limit: limit * multiplier, as_of: as_of)
112
125
  include_inferences = opts.fetch(:include_inferences, true)
113
126
  include_history = opts.fetch(:include_history, false)
114
- candidates = filter_candidates(candidates, min_confidence: min_confidence, tags: tags,
115
- options: { include_inferences: include_inferences,
116
- include_history: include_history, as_of: as_of,
117
- requesting_principal_id: requesting_principal_id })
127
+ candidates = fts_search(text, as_of: as_of) do |dataset|
128
+ filter_candidates(dataset.lazy, min_confidence: min_confidence, tags: tags,
129
+ options: { include_inferences: include_inferences,
130
+ include_history: include_history, as_of: as_of,
131
+ requesting_principal_id: requesting_principal_id })
132
+ .take(limit * multiplier).force
133
+ end
118
134
  candidates = cosine_rerank(text, candidates) if can_rerank?
119
135
  results = candidates.first(limit)
120
136
 
@@ -146,22 +162,9 @@ module Legion
146
162
  def reset!
147
163
  LIFECYCLE_MUTEX.synchronize do
148
164
  @started = false
149
- @seeded = false
150
165
  end
151
166
  end
152
167
 
153
- def seed_self_knowledge
154
- return unless started?
155
-
156
- SEED_MUTEX.synchronize { seed_self_knowledge_without_lock }
157
- rescue StandardError => e
158
- handle_exception(e, level: :warn, operation: 'apollo.local.seed_self_knowledge')
159
- end
160
-
161
- def seeded?
162
- @seeded == true
163
- end
164
-
165
168
  def query_by_tags(tags:, limit: 50) # rubocop:disable Metrics/MethodLength
166
169
  connection = local_db_connection
167
170
  tags = normalize_tags_input(tags)
@@ -232,15 +235,6 @@ module Legion
232
235
  { success: false, error: e.message }
233
236
  end
234
237
 
235
- def hydrate_from_global
236
- return { success: false, error: :not_started } unless started?
237
-
238
- HYDRATION_MUTEX.synchronize { hydrate_from_global_without_lock }
239
- rescue StandardError => e
240
- handle_exception(e, level: :error, operation: 'apollo.local.hydrate_from_global')
241
- { success: false, error: e.message }
242
- end
243
-
244
238
  def version_chain(entry_id:, max_depth: 50) # rubocop:disable Metrics/MethodLength
245
239
  return not_started_error unless started?
246
240
 
@@ -278,49 +272,6 @@ module Legion
278
272
 
279
273
  private
280
274
 
281
- def self_knowledge_files
282
- seed_dir = File.join(File.expand_path('../../..', __dir__), 'data', 'self-knowledge')
283
- return [] unless File.directory?(seed_dir)
284
-
285
- Dir[File.join(seed_dir, '*.md')]
286
- end
287
-
288
- def seed_files(files)
289
- count = 0
290
- files.each do |path|
291
- count += 1 if seed_single_file(path)
292
- end
293
- count
294
- end
295
-
296
- def seed_single_file(path)
297
- content = File.read(path)
298
- return false if content.strip.empty?
299
-
300
- tags = ['legionio', 'self-knowledge', File.basename(path, '.md')]
301
- result = ingest(content: content, tags: tags, source_channel: 'self-knowledge',
302
- submitted_by: 'legion-apollo', confidence: 0.9)
303
- return false unless result[:success] && result[:mode] != :deduplicated
304
-
305
- ingest_global(content: content, tags: tags) if global_available?
306
- true
307
- end
308
-
309
- def ingest_global(content:, tags:)
310
- log.debug { "Apollo::Local forwarding seed entry to global tag_count=#{Array(tags).size}" }
311
- Legion::Apollo.ingest(content: content, tags: tags, source_channel: 'self-knowledge',
312
- submitted_by: 'legion-apollo', confidence: 0.9, scope: :global)
313
- rescue StandardError => e
314
- handle_exception(e, level: :debug, operation: 'apollo.local.ingest_global_seed', tag_count: Array(tags).size)
315
- end
316
-
317
- def global_available?
318
- defined?(Legion::Apollo) && Legion::Apollo.started? && Legion::Apollo.respond_to?(:ingest)
319
- rescue StandardError => e
320
- handle_exception(e, level: :debug, operation: 'apollo.local.global_available')
321
- false
322
- end
323
-
324
275
  def local_enabled?
325
276
  return false unless defined?(Legion::Settings)
326
277
 
@@ -372,13 +323,6 @@ module Legion
372
323
  Digest::MD5.hexdigest(normalized)
373
324
  end
374
325
 
375
- def duplicate?(hash)
376
- db[:local_knowledge].where(content_hash: hash).any?
377
- rescue StandardError => e
378
- handle_exception(e, level: :debug, operation: 'apollo.local.duplicate_check', hash: hash)
379
- false
380
- end
381
-
382
326
  def start_without_lock
383
327
  return if @started
384
328
  return unless local_enabled? && data_local_available?
@@ -388,63 +332,12 @@ module Legion
388
332
  log.info 'Legion::Apollo::Local started'
389
333
  end
390
334
 
391
- def seed_self_knowledge_without_lock
392
- return if @seeded
393
-
394
- files = self_knowledge_files
395
- return if files.empty?
396
-
397
- count = seed_files(files)
398
- @seeded = true
399
- log.info("Apollo::Local seeded #{count} self-knowledge files")
400
- end
401
-
402
- def hydrate_from_global_without_lock # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
403
- local_check = query_by_tags(tags: ['partner'])
404
- if local_check[:success] && local_check[:results]&.any?
405
- log.info 'Apollo::Local hydration skipped because local partner data already exists'
406
- return { success: true, skipped: :local_data_exists }
407
- end
408
-
409
- unless Legion::Apollo.transport_available? || Legion::Apollo.data_available?
410
- log.info 'Apollo::Local hydration skipped because global Apollo is unavailable'
411
- return { success: true, skipped: :global_unavailable }
412
- end
413
-
414
- global_entries = Legion::Apollo.retrieve(text: 'partner bond', scope: :global, limit: 20)
415
- entries = Array(global_entries[:entries] || global_entries[:results])
416
- unless global_entries[:success] && entries.any?
417
- log.info 'Apollo::Local hydration skipped because no global partner data was found'
418
- return { success: true, skipped: :no_global_data }
419
- end
420
-
421
- log.info { "Apollo::Local hydration started global_count=#{entries.size}" }
422
- hydrated = 0
423
- entries.each do |entry|
424
- entry_tags = entry[:tags].is_a?(Array) ? entry[:tags] : []
425
- clean_tags = entry_tags.reject { |tag| tag == 'promoted_from_local' } + ['hydrated_from_global']
426
-
427
- result = ingest(
428
- content: entry[:content],
429
- raw_content: entry[:raw_content] || entry[:content],
430
- tags: clean_tags,
431
- confidence: ((entry[:confidence] || 0.5) * 0.9).round(10),
432
- source_channel: 'global_hydration',
433
- valid_from: entry[:valid_from],
434
- valid_to: entry[:valid_to]
435
- )
436
- hydrated += 1 if result[:success]
437
- end
438
-
439
- log.info { "Apollo::Local hydration completed hydrated=#{hydrated}" }
440
- { success: true, hydrated: hydrated }
441
- end
442
-
443
- def ingest_without_lock(content:, tags:, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
335
+ def ingest_without_lock(content:, tags:, embedding_fields:, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
444
336
  content = normalize_text_input(content)
445
337
  raw_content = normalize_raw_content_input(opts[:raw_content], fallback: content)
446
338
  hash = content_hash(content)
447
- return deduplicated_ingest(hash) if duplicate?(hash)
339
+ duplicate = deduplicated_ingest(hash, expires_at: opts[:expires_at])
340
+ return duplicate if duplicate
448
341
 
449
342
  log.info do
450
343
  "Apollo::Local ingest accepted content_length=#{content.to_s.length} " \
@@ -455,15 +348,17 @@ module Legion
455
348
  metadata = opts.dup
456
349
  metadata.delete(:raw_content)
457
350
  row = build_ingest_row(content: content, raw_content: raw_content, hash: hash, tags: tags, **metadata)
351
+ .merge(embedding_fields)
458
352
  id = persist_ingest_row(row, metadata)
459
353
  mark_parent_superseded(metadata[:parent_knowledge_id]) if metadata[:parent_knowledge_id]
460
354
 
461
355
  log.info { "Apollo::Local ingest stored id=#{id} hash=#{hash}" }
462
356
  { success: true, mode: :local, id: id }
463
357
  rescue Sequel::UniqueConstraintViolation
464
- raise unless duplicate?(hash)
358
+ duplicate = deduplicated_ingest(hash, expires_at: opts[:expires_at])
359
+ raise unless duplicate
465
360
 
466
- deduplicated_ingest(hash)
361
+ duplicate
467
362
  end
468
363
 
469
364
  def build_ingest_row(content:, raw_content:, hash:, tags:, **opts) # rubocop:disable Metrics/MethodLength
@@ -477,7 +372,7 @@ module Legion
477
372
  opts: opts,
478
373
  is_inference: is_inference,
479
374
  default_confidence: default_confidence
480
- ).merge(embedding_columns(content, opts)).merge(timestamp_columns)
375
+ ).merge(timestamp_columns)
481
376
  end
482
377
 
483
378
  def ingest_metadata_columns(context)
@@ -545,7 +440,17 @@ module Legion
545
440
  handle_exception(e, level: :warn, operation: 'apollo.local.create_source_link', entry_id: entry_id)
546
441
  end
547
442
 
548
- def deduplicated_ingest(hash)
443
+ def deduplicated_ingest(hash, expires_at: nil) # rubocop:disable Metrics/MethodLength
444
+ dataset = db[:local_knowledge].where(content_hash: hash)
445
+ existing = dataset.select(:expires_at).first
446
+ return unless existing
447
+
448
+ now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
449
+ previous_expiry = existing[:expires_at]
450
+ if previous_expiry && previous_expiry <= now
451
+ dataset.where(Sequel.lit('expires_at <= ?', now))
452
+ .update(expires_at: expires_at || compute_expires_at, updated_at: now)
453
+ end
549
454
  log.info { "Apollo::Local ingest deduplicated hash=#{hash}" }
550
455
  { success: true, mode: :deduplicated }
551
456
  end
@@ -615,33 +520,31 @@ module Legion
615
520
  Legion::JSON.dump(normalize_tags_input(tags))
616
521
  end
617
522
 
618
- def fts_search(text, limit:, as_of: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
523
+ def fts_search(text, as_of: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
619
524
  now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
620
- return active_knowledge_dataset(now: now, as_of: as_of).limit(limit).all if text.to_s.strip.empty?
525
+ return yield active_knowledge_dataset(now: now, as_of: as_of) if text.to_s.strip.empty?
621
526
 
622
527
  tokens = text.to_s.scan(/[\p{L}\p{N}_]+/)
623
- return ilike_search(text, now: now, limit: limit, as_of: as_of) if tokens.empty?
528
+ return yield ilike_search(text, now: now, as_of: as_of) if tokens.empty?
624
529
 
625
530
  escaped = tokens.map { |t| %("#{t}") }.join(' ')
626
531
  temporal_sql, temporal_params = temporal_window_sql(as_of, table_alias: 'lk')
627
- db.fetch(
532
+ yield db.fetch(
628
533
  'SELECT lk.* FROM local_knowledge lk ' \
629
534
  'INNER JOIN local_knowledge_fts fts ON lk.id = fts.rowid ' \
630
535
  "WHERE local_knowledge_fts MATCH ? AND lk.expires_at > ?#{temporal_sql} " \
631
- 'ORDER BY fts.rank LIMIT ?',
632
- escaped, now, *temporal_params, limit
633
- ).all
536
+ 'ORDER BY fts.rank',
537
+ escaped, now, *temporal_params
538
+ )
634
539
  rescue StandardError => e
635
- handle_exception(e, level: :debug, operation: 'apollo.local.fts_search', limit: limit, fallback: :ilike)
636
- ilike_search(text, now: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), limit: limit, as_of: as_of)
540
+ handle_exception(e, level: :debug, operation: 'apollo.local.fts_search', fallback: :ilike)
541
+ yield ilike_search(text, now: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), as_of: as_of)
637
542
  end
638
543
 
639
- def ilike_search(text, now:, limit:, as_of: nil)
544
+ def ilike_search(text, now:, as_of: nil)
640
545
  safe_text = text.to_s.gsub('\\', '\\\\\\\\').gsub('%', '\%').gsub('_', '\_')
641
546
  active_knowledge_dataset(now: now, as_of: as_of)
642
547
  .where(Sequel.lit("content LIKE ? ESCAPE '\\' COLLATE NOCASE", "%#{safe_text}%"))
643
- .limit(limit)
644
- .all
645
548
  end
646
549
 
647
550
  def filter_candidates(candidates, min_confidence:, tags:, options: {}) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/AbcSize
@@ -847,20 +750,26 @@ module Legion
847
750
  end.first(limit)
848
751
  end
849
752
 
850
- def update_upsert_entry(existing, content, tags_json, opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
753
+ def update_upsert_entry(existing, content, tags_json, opts, embedding_fields) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
851
754
  content = normalize_text_input(content)
852
755
  new_hash = content_hash(content)
853
- embedding, embedded_at = generate_embedding(content)
854
756
  now = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
855
757
  expires_at = compute_expires_at
758
+ raw_content = opts.fetch(:raw_content) do
759
+ existing[:raw_content] unless existing[:raw_content] == existing[:content]
760
+ end
856
761
 
857
762
  db.transaction do
763
+ db.run("DELETE FROM local_knowledge_fts WHERE rowid = #{existing[:id]}")
858
764
  db[:local_knowledge].where(id: existing[:id]).update(
859
765
  content: content,
766
+ raw_content: normalize_raw_content_input(raw_content, fallback: content),
767
+ valid_from: normalize_temporal_value(opts.fetch(:valid_from, existing[:valid_from])),
768
+ valid_to: normalize_temporal_value(opts.fetch(:valid_to, existing[:valid_to])),
860
769
  content_hash: new_hash,
861
770
  tags: tags_json,
862
- embedding: embedding ? Legion::JSON.dump(embedding) : nil,
863
- embedded_at: embedded_at,
771
+ embedding: embedding_fields[:embedding],
772
+ embedded_at: embedding_fields[:embedded_at],
864
773
  confidence: opts.fetch(:confidence, existing[:confidence]),
865
774
  expires_at: expires_at,
866
775
  source_channel: opts.fetch(:source_channel, existing[:source_channel]),
@@ -872,18 +781,12 @@ module Legion
872
781
  identity_id: opts.fetch(:identity_id, existing[:identity_id]),
873
782
  updated_at: now
874
783
  )
875
- rebuild_fts_entry!(existing[:id], content, tags_json)
784
+ sync_fts!(existing[:id], content, tags_json)
876
785
  end
877
786
  log.info { "Apollo::Local upsert updated id=#{existing[:id]} hash=#{new_hash}" }
878
787
  { success: true, mode: :updated, id: existing[:id] }
879
788
  end
880
789
 
881
- def rebuild_fts_entry!(id, content, tags_json)
882
- db.run("DELETE FROM local_knowledge_fts WHERE rowid = #{id}")
883
- sync_fts!(id, content, tags_json)
884
- log.debug { "Apollo::Local FTS rebuilt id=#{id}" }
885
- end
886
-
887
790
  def project_tier(entry, tier) # rubocop:disable Metrics/MethodLength
888
791
  case tier
889
792
  when :l0
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Apollo
5
- VERSION = '0.5.6'
5
+ VERSION = '0.5.13'
6
6
  end
7
7
  end
data/lib/legion/apollo.rb CHANGED
@@ -39,9 +39,6 @@ module Legion
39
39
 
40
40
  @started = true
41
41
  log.info 'Legion::Apollo started'
42
-
43
- seed_self_knowledge
44
- Legion::Apollo::Local.hydrate_from_global if Legion::Apollo::Local.started?
45
42
  end
46
43
  rescue StandardError => e
47
44
  handle_exception(e, level: :error, operation: 'apollo.start')
@@ -372,25 +369,28 @@ module Legion
372
369
  payload.merge(text: normalized_text, query: normalized_text)
373
370
  end
374
371
 
375
- def normalize_local_entries(entries) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
372
+ def normalize_local_entries(entries) # rubocop:disable Metrics/AbcSize
376
373
  entries.map do |e|
377
- hash = e[:content_hash] || Digest::MD5.hexdigest(e[:content].to_s.strip.downcase.gsub(/\s+/, ' '))
378
- tags = if e[:tags].is_a?(String)
379
- begin
380
- Legion::JSON.parse(e[:tags])
381
- rescue StandardError => ex
382
- handle_exception(ex, level: :debug, operation: 'apollo.normalize_local_entries', entry_id: e[:id])
383
- []
384
- end
385
- else
386
- Array(e[:tags])
387
- end
388
- { id: e[:id], content: e[:content], raw_content: e[:raw_content] || e[:content], content_hash: hash,
389
- confidence: e[:confidence] || 0.5, content_type: 'fact', tags: tags, source: :local,
390
- valid_from: e[:valid_from], valid_to: e[:valid_to] }
374
+ content = e[:content] || e[:summary]
375
+ hash = e[:content_hash] || Digest::MD5.hexdigest(content.to_s.strip.downcase.gsub(/\s+/, ' '))
376
+ tags = normalize_local_entry_tags(e)
377
+ entry = { id: e[:id], content: content, raw_content: e[:raw_content] || content, content_hash: hash,
378
+ confidence: e[:confidence] || 0.5, content_type: 'fact', tags: tags, source: :local,
379
+ valid_from: e[:valid_from], valid_to: e[:valid_to] }
380
+ entry[:summary] = e[:summary] if e.key?(:summary)
381
+ entry
391
382
  end
392
383
  end
393
384
 
385
+ def normalize_local_entry_tags(entry)
386
+ return Array(entry[:tags]) unless entry[:tags].is_a?(String)
387
+
388
+ Legion::JSON.parse(entry[:tags])
389
+ rescue StandardError => e
390
+ handle_exception(e, level: :debug, operation: 'apollo.normalize_local_entries', entry_id: entry[:id])
391
+ []
392
+ end
393
+
394
394
  def normalize_global_entries(entries)
395
395
  entries.map { |entry| normalize_global_entry(entry) }
396
396
  end
@@ -469,13 +469,6 @@ module Legion
469
469
  { success: false, error: e.message }
470
470
  end
471
471
 
472
- def seed_self_knowledge
473
- log.info 'Apollo self-knowledge seed requested'
474
- Legion::Apollo::Local.seed_self_knowledge if Legion::Apollo::Local.started?
475
- rescue StandardError => e
476
- handle_exception(e, level: :warn, operation: 'apollo.seed_self_knowledge')
477
- end
478
-
479
472
  def apollo_setting(key, default)
480
473
  return default unless defined?(Legion::Settings) && !Legion::Settings[:apollo].nil?
481
474
 
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.5.6
4
+ version: 0.5.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -66,17 +66,6 @@ files:
66
66
  - CHANGELOG.md
67
67
  - LICENSE
68
68
  - README.md
69
- - data/self-knowledge/01-what-is-legion.md
70
- - data/self-knowledge/02-architecture.md
71
- - data/self-knowledge/03-extensions.md
72
- - data/self-knowledge/04-security.md
73
- - data/self-knowledge/05-llm-pipeline.md
74
- - data/self-knowledge/06-apollo-knowledge.md
75
- - data/self-knowledge/07-cli-reference.md
76
- - data/self-knowledge/08-cognitive-layer.md
77
- - data/self-knowledge/09-teams-integration.md
78
- - data/self-knowledge/10-deployment.md
79
- - data/self-knowledge/11-my-partner.md
80
69
  - lib/legion/apollo.rb
81
70
  - lib/legion/apollo/helpers/confidence.rb
82
71
  - lib/legion/apollo/helpers/similarity.rb
@@ -1,51 +0,0 @@
1
- # What is LegionIO?
2
-
3
- LegionIO is an extensible async job engine and cognitive platform for Ruby. It schedules tasks, creates relationships between services, and runs them concurrently. It was created by Matthew Iverson (@Esity) and is licensed under Apache-2.0.
4
-
5
- LegionIO is not a chatbot. It is a framework that can power chatbots, AI assistants, background workers, service integrations, and autonomous agents. The chat interface is one of many ways to interact with it.
6
-
7
- ## Core Purpose
8
-
9
- LegionIO connects isolated systems — cloud accounts, on-premise services, SaaS tools — into a unified async task engine. It uses RabbitMQ for message passing, supports SQLite/PostgreSQL/MySQL for persistence, and Redis/Memcached for caching.
10
-
11
- ## What LegionIO Does
12
-
13
- - Schedules and executes async tasks across distributed services
14
- - Chains tasks into workflows (Task A -> conditioner -> Task B -> transformer -> Task C)
15
- - Auto-discovers and loads extension gems (LEX plugins) at boot
16
- - Provides a unified REST API on port 4567 for all operations
17
- - Integrates with HashiCorp Vault for secrets and authentication
18
- - Supports Kerberos auto-authentication to Vault using existing AD credentials
19
- - Runs a 19-step LLM pipeline with RAG, guardrails, cost tracking, and model routing
20
- - Maintains a shared knowledge store (Apollo) for organizational knowledge
21
- - Provides a rich terminal UI with AI chat, dashboard, and extension browser
22
- - Tracks token usage and costs per-user and per-team
23
- - Supports HIPAA PHI compliance with redaction, crypto-erasure, and audit trails
24
- - Runs as a macOS/Linux background service via Homebrew or systemd
25
-
26
- ## What LegionIO Does Not Do
27
-
28
- - It does not provide direct cloud infrastructure (no VMs, no networking)
29
- - It does not replace Terraform, Ansible, or Chef for infrastructure management
30
- - It does not host web applications or serve static content
31
- - It does not provide its own LLM — it routes to providers like Bedrock, Anthropic, OpenAI, Gemini, or Ollama
32
- - It does not require RabbitMQ in lite mode (uses an in-process message adapter)
33
- - It does not store credentials on disk — all secrets are in Vault or environment variables
34
-
35
- ## Installation
36
-
37
- Install via Homebrew on macOS:
38
- ```
39
- brew tap legionio/tap
40
- brew install legionio
41
- ```
42
-
43
- Or via RubyGems:
44
- ```
45
- gem install legionio
46
- ```
47
-
48
- ## Key Binaries
49
-
50
- - `legionio` — daemon and operational CLI (start, stop, config, lex, task, mcp, etc.)
51
- - `legion` — interactive terminal shell with AI chat, onboarding wizard, and dashboard
@@ -1,51 +0,0 @@
1
- # LegionIO Architecture
2
-
3
- ## Boot Sequence
4
-
5
- LegionIO starts subsystems in a fixed order. Each phase is individually toggleable.
6
-
7
- 1. Logging (legion-logging)
8
- 2. Settings (legion-settings — loads from /etc/legionio, ~/.legionio, ./settings)
9
- 3. Crypt (legion-crypt — Vault connection, Kerberos auto-auth)
10
- 4. Transport (legion-transport — RabbitMQ or InProcess lite adapter)
11
- 5. Cache (legion-cache — Redis, Memcached, or Memory adapter)
12
- 6. Data (legion-data — SQLite, PostgreSQL, or MySQL via Sequel)
13
- 7. RBAC (legion-rbac — role-based access control)
14
- 8. LLM (legion-llm — AI provider setup and routing)
15
- 9. Apollo (legion-apollo — shared and local knowledge store)
16
- 10. GAIA (legion-gaia — cognitive coordination layer, 24 phases)
17
- 11. Telemetry (OpenTelemetry tracing, optional)
18
- 12. Extensions (two-phase parallel: require+autobuild, then hook actors)
19
- 13. API (Sinatra/Puma REST API on port 4567)
20
-
21
- Shutdown runs in reverse order. Reload shuts down then re-runs from settings onward.
22
-
23
- ## Core Gems
24
-
25
- | Gem | Purpose |
26
- |-----|---------|
27
- | legion-transport | RabbitMQ AMQP messaging + InProcess lite adapter |
28
- | legion-cache | Caching (Redis/Memcached/Memory) |
29
- | legion-crypt | Encryption, Vault integration, JWT, Kerberos auth, mTLS |
30
- | legion-data | Database persistence via Sequel (SQLite/PostgreSQL/MySQL) |
31
- | legion-json | JSON serialization (multi_json wrapper) |
32
- | legion-logging | Console + structured JSON logging with redaction |
33
- | legion-settings | Configuration management with schema validation |
34
- | legion-llm | LLM integration with 19-step pipeline |
35
- | legion-mcp | MCP server with 58+ tools |
36
- | legion-gaia | Cognitive coordination (24 phases: 16 active + 8 dream) |
37
- | legion-apollo | Shared knowledge store client (local SQLite + global pgvector) |
38
- | legion-rbac | Role-based access control with Vault-style policies |
39
- | legion-tty | Rich terminal UI with AI chat and operational dashboard |
40
-
41
- ## Extension Loading
42
-
43
- Extensions are gems named `lex-*`, auto-discovered via Bundler or Gem::Specification. Loading is two-phase and parallel: all extensions are required and `autobuild` runs concurrently on a thread pool, then `hook_all_actors` starts subscriptions sequentially. This prevents race conditions.
44
-
45
- ## Lite Mode
46
-
47
- Setting `LEGION_MODE=lite` replaces RabbitMQ with an InProcess adapter and Redis with a Memory adapter. No external infrastructure required. Useful for development, demos, and single-machine deployments.
48
-
49
- ## REST API
50
-
51
- Full REST API served by Sinatra/Puma on port 4567. Endpoints include tasks, extensions, runners, nodes, schedules, relationships, settings, events (SSE), transport status, hooks, workers, teams, capacity, tenants, audit, RBAC, and webhooks. JWT Bearer auth middleware with rate limiting.
@@ -1,54 +0,0 @@
1
- # LegionIO Extension System (LEX)
2
-
3
- ## What is a LEX?
4
-
5
- A LEX (Legion Extension) is a Ruby gem named `lex-*` that plugs into LegionIO. Each LEX defines runners (functions) and actors (execution modes). Extensions are auto-discovered at boot — install a gem and it loads automatically.
6
-
7
- ## Actor Types
8
-
9
- | Type | Behavior |
10
- |------|----------|
11
- | Subscription | Consumes messages from an AMQP queue |
12
- | Polling | Polls on a schedule |
13
- | Interval (Every) | Runs at a fixed interval |
14
- | Once | Runs once at startup |
15
- | Loop | Runs continuously |
16
- | Nothing | Passive — only invoked via API or other extensions |
17
-
18
- ## Creating an Extension
19
-
20
- ```bash
21
- legion lex create myextension # scaffold a new lex-myextension gem
22
- legion generate runner myrunner # add a runner with functions
23
- legion generate actor myactor # add an actor with type selection
24
- legion generate tool mytool # add an MCP tool
25
- ```
26
-
27
- ## Extension Categories
28
-
29
- ### Core Operational (21 extensions)
30
- node, tasker, scheduler, synapse, LLM gateway, detect, telemetry, acp, react, webhook, health, metering, exec, conditioner, transformer, tick, audit, codegen, privatecore, lex (meta), knowledge
31
-
32
- ### Agentic/Cognitive (13 consolidated gems + supporting)
33
- self (identity, metacognition, reflection, personality, agency), affect (emotion, mood, sentiment), imagination (creative generation, dream ideation), language (NLU, discourse), memory (episodic, semantic, working memory), social (theory of mind, social cognition), swarm-github (code review), mesh (inter-agent communication), mind-growth (autonomous expansion), autofix, dataset, eval, factory
34
-
35
- ### AI Provider Integrations (7)
36
- azure-ai, bedrock, claude, foundry, gemini, openai, xai
37
-
38
- ### Service Integrations (10 common + 40 additional)
39
- Common: consul, github, http, kerberos, vault, tfe, microsoft-teams, slack, webhook, acp
40
- Additional: chef, jfrog, ssh, smtp, kafka, jira, docker, kubernetes, and more
41
-
42
- ## Role-Based Filtering
43
-
44
- Extensions load based on role profile:
45
- - `nil` (default): all extensions
46
- - `:core`: 14 core operational only
47
- - `:cognitive`: core + all agentic
48
- - `:service`: core + service integrations
49
- - `:dev`: core + AI + essential agentic
50
- - `:custom`: explicit list from settings
51
-
52
- ## Extension Discovery
53
-
54
- At boot, LegionIO calls `Bundler.load.specs` (or `Gem::Specification` fallback) to find all `lex-*` gems. Each extension's `autobuild` creates runners and actors. After all extensions load, `hook_all_actors` activates AMQP subscriptions and timers.
@@ -1,46 +0,0 @@
1
- # LegionIO Security
2
-
3
- ## Authentication
4
-
5
- ### Kerberos + Vault
6
- LegionIO authenticates to HashiCorp Vault using Kerberos (SPNEGO). On macOS or Linux machines joined to Active Directory, the existing Kerberos ticket is used — no password entry needed. The SPNEGO token is sent as an HTTP Authorization header to Vault's Kerberos auth backend, which returns a Vault token. Token renewal runs in a background thread at 75% TTL.
7
-
8
- ### JWT Authentication
9
- The REST API uses JWT Bearer auth. Tokens are validated against JWKS endpoints. Skip paths exist for health and readiness checks.
10
-
11
- ### mTLS
12
- Optional mutual TLS for internal communications. Vault PKI issues certificates, and a background thread rotates them at 50% TTL. Feature-flagged via `security.mtls.enabled`.
13
-
14
- ## Secrets Management
15
-
16
- All secrets are stored in HashiCorp Vault, never on disk. Config files reference secrets using `vault://` URIs that are resolved at runtime. Environment variable fallback is supported via `env://` URIs.
17
-
18
- Example: `"bearer_token": "vault://secret/data/llm/bedrock#bearer_token"`
19
-
20
- ## RBAC
21
-
22
- Optional role-based access control using Vault-style flat policies. Policies map identities to allowed actions on resources. Enforced at the API middleware layer and in the LLM pipeline.
23
-
24
- ## HIPAA PHI Compliance
25
-
26
- - **PHI Tagging**: Metadata classification for sensitive data
27
- - **PHI Access Logging**: Audit trail via Legion::Audit for all PHI access
28
- - **PHI Erasure**: Crypto-erasure orchestration via Crypt::Erasure + Cache purge
29
- - **PHI TTL Cap**: legion-cache enforces maximum TTL for PHI-tagged data
30
- - **Redaction**: Automatic PII/PHI redaction in all log output via legion-logging
31
-
32
- All PHI features are off by default and enabled via configuration.
33
-
34
- ## Audit
35
-
36
- - Tamper-evident hash chain for audit entries
37
- - 7-year tiered retention (hot -> warm -> cold storage)
38
- - SIEM export for Splunk/ELK ingestion
39
- - Queryable via CLI (`legion audit`) and REST API
40
-
41
- ## Network Security
42
-
43
- - No public IPs or ingress in production deployments
44
- - TLS required on all connections (Optum-sanctioned CAs only in UHG deployments)
45
- - Rate limiting middleware with per-IP/agent/tenant tiers
46
- - Request body size limits (1MB max)
@@ -1,54 +0,0 @@
1
- # LegionIO LLM Pipeline
2
-
3
- ## Overview
4
-
5
- LegionIO routes AI requests through a 19-step pipeline that adds governance, RAG context, tool use, cost tracking, and knowledge capture. The pipeline is provider-agnostic — it works with Bedrock, Anthropic, OpenAI, Gemini, and local Ollama.
6
-
7
- ## Pipeline Steps
8
-
9
- 1. **Normalize** — standardize request format
10
- 2. **Profile** — derive caller profile (user, system, service)
11
- 3. **RBAC** — check access permissions
12
- 4. **Classification** — classify request sensitivity
13
- 5. **Billing** — check budget and rate limits
14
- 6. **Guardrails** — input validation and safety checks
15
- 7. **GAIA Advisory** — cognitive layer enrichment (optional system prompt)
16
- 8. **RAG Context** — retrieve relevant knowledge from Apollo (global + local)
17
- 9. **MCP Discovery** — discover available tools
18
- 10. **Enrichment Injection** — prepend GAIA/RAG context to system prompt
19
- 11. **Fleet Selection** — choose optimal model/provider
20
- 12. **Dispatch** — send request to LLM provider
21
- 13. **Parse Response** — extract text and tool calls
22
- 14. **Tool Calls** — execute MCP tools if requested
23
- 15. **Post-Response** — post-processing
24
- 16. **Audit** — publish audit trail
25
- 17. **Metering** — record token usage and cost
26
- 18. **Timeline** — record timing data
27
- 19. **Knowledge Capture** — write significant responses back to Apollo
28
-
29
- ## Supported Providers
30
-
31
- | Provider | Models | Auth |
32
- |----------|--------|------|
33
- | AWS Bedrock | Claude, Llama, Mistral | Bearer token (from Vault) |
34
- | Anthropic | Claude family | API key |
35
- | OpenAI | GPT-4, GPT-3.5 | API key |
36
- | Google Gemini | Gemini Pro, Flash | API key |
37
- | Ollama | Any local model | None (localhost) |
38
-
39
- ## Cost Tracking
40
-
41
- Every LLM call is metered. Token counts (input/output) and estimated costs are tracked per-request, per-session, per-user, and per-team. The status bar in the terminal UI shows real-time token count and cost. Budget limits can be set per-user or per-team.
42
-
43
- ## Model Routing
44
-
45
- The fleet selection step chooses the optimal model based on request classification, cost constraints, and provider availability. Model escalation automatically retries with a more capable model if the initial response fails quality checks.
46
-
47
- ## RAG Integration
48
-
49
- Step 8 retrieves relevant context from Apollo using scope routing:
50
- - `:local` — node-local SQLite+FTS5 store only
51
- - `:global` — shared PostgreSQL+pgvector store
52
- - `:all` — both merged, deduplicated by content hash, ranked by confidence
53
-
54
- Retrieved context is injected into the system prompt by the Enrichment Injector (step 10).
@@ -1,54 +0,0 @@
1
- # Apollo Knowledge Store
2
-
3
- ## What is Apollo?
4
-
5
- Apollo is LegionIO's shared knowledge store. It provides organizational memory that persists across sessions and is shared across all LegionIO nodes. Every AI response can reference knowledge from Apollo, and significant responses are captured back into it.
6
-
7
- ## Architecture
8
-
9
- ### Local Store (every node)
10
- - SQLite database with FTS5 full-text search
11
- - Content-hash deduplication (MD5)
12
- - Optional LLM embeddings (1024-dim) with cosine rerank
13
- - TTL-based expiry (default 5 years)
14
- - Works offline — no network required
15
-
16
- ### Global Store (shared)
17
- - PostgreSQL with pgvector extension
18
- - HNSW cosine similarity index
19
- - Agents interact via RabbitMQ (no direct DB access)
20
- - Hosted on Azure PostgreSQL Flexible Server
21
-
22
- ## Scope Routing
23
-
24
- All queries and ingests accept a `scope:` parameter:
25
- - `:local` — SQLite only
26
- - `:global` — PostgreSQL only (via transport or co-located extension)
27
- - `:all` — both merged, deduplicated by content hash, ranked by confidence
28
-
29
- ## Knowledge Capture
30
-
31
- The LLM pipeline's step 19 (Knowledge Capture) automatically writes significant responses back to Apollo. This creates a feedback loop where the system learns from its own interactions. Content-hash deduplication prevents echo chambers.
32
-
33
- ## Knowledge CLI
34
-
35
- ```
36
- legion knowledge query "question" # query and synthesize answer
37
- legion knowledge retrieve "question" # raw source chunks
38
- legion knowledge ingest <path> # ingest file or directory
39
- legion knowledge status # corpus stats
40
- legion knowledge health # full health report
41
- legion knowledge maintain # orphan detection and cleanup
42
- legion knowledge quality # quality report
43
- legion knowledge monitor add <path> # watch a directory for changes
44
- legion knowledge capture commit # capture git commit as knowledge
45
- ```
46
-
47
- ## Content Pipeline
48
-
49
- Files ingested via `legion knowledge ingest` go through:
50
- 1. Format detection (Markdown, PDF, DOCX, plain text)
51
- 2. Chunking by heading hierarchy (H1-H6 with ancestry path)
52
- 3. Delta detection (only new/changed files via manifest)
53
- 4. Batch embedding (one LLM call per file, not per chunk)
54
- 5. Upsert to Apollo (local and/or global based on scope)
@@ -1,59 +0,0 @@
1
- # LegionIO CLI Reference
2
-
3
- ## Interactive Shell
4
-
5
- Running `legion` with no arguments launches the rich terminal UI:
6
- - Digital rain intro animation on first run
7
- - Onboarding wizard with Kerberos identity detection
8
- - AI chat shell with streaming responses
9
- - Dashboard (Ctrl+D) with service status panels
10
- - Extension browser, config editor, command palette (Ctrl+K)
11
- - 115+ slash commands, tab completion, session persistence
12
-
13
- ## Key Commands
14
-
15
- ### Daemon Operations (legionio)
16
- ```
17
- legionio start # start daemon
18
- legionio stop # stop daemon
19
- legionio status # check daemon status
20
- legionio doctor # 11-check environment diagnosis
21
- legionio config scaffold # generate starter config files
22
- legionio config import <url> # import config from URL
23
- legionio bootstrap <url> # one-command setup (config + scaffold + install packs)
24
- legionio setup agentic # install 47 cognitive gems
25
- legionio setup claude-code # configure MCP server for Claude Code
26
- legionio setup cursor # configure MCP server for Cursor
27
- legionio mcp stdio # start MCP server (stdio transport)
28
- legionio lex list # list loaded extensions
29
- legionio update # self-update via Homebrew or gem
30
- ```
31
-
32
- ### Interactive / Dev (legion)
33
- ```
34
- legion # launch rich terminal UI
35
- legion chat # AI chat REPL
36
- legion do "natural language" # natural language command routing
37
- legion knowledge query "question" # query knowledge base
38
- legion commit # AI-generated commit message
39
- legion pr # AI-generated PR description
40
- legion review # AI code review
41
- legion plan # read-only exploration mode
42
- legion memory list # persistent memory management
43
- legion mind-growth status # cognitive architecture status
44
- ```
45
-
46
- ### MCP Server
47
-
48
- LegionIO exposes 58+ MCP tools when configured as an MCP server in Claude Code, Cursor, or VS Code. Tools cover knowledge queries, extension management, task operations, system status, and more.
49
-
50
- ## Natural Language Commands
51
-
52
- `legion do` routes free-text to the right extension capability:
53
- ```
54
- legion do "list all running extensions"
55
- legion do "check system health"
56
- legion do "show vault status"
57
- ```
58
-
59
- It tries three resolution paths: daemon API, in-process capability registry, LLM classification.
@@ -1,49 +0,0 @@
1
- # LegionIO Cognitive Layer
2
-
3
- ## GAIA (Cognitive Coordination)
4
-
5
- GAIA is LegionIO's cognitive coordination layer. It manages 24 phases (16 active + 8 dream) in a tick-based cycle. Each tick runs through phases like sensory processing, attention, working memory integration, prediction, emotional evaluation, action selection, and reflection.
6
-
7
- ### Active Phases (16)
8
- sensory_processing, attention_filtering, working_memory_integration, contextual_memory_retrieval, knowledge_retrieval, prediction_engine, emotional_evaluation, action_selection, social_cognition, theory_of_mind, homeostasis_regulation, autonomy_gating, identity_entropy_check, post_tick_reflection, audit_publish, metering
9
-
10
- ### Dream Phases (8)
11
- memory_consolidation, dream_generation, emotional_processing, creative_association, pattern_extraction, belief_update, schema_integration, dream_ideation
12
-
13
- ## Agentic Extensions
14
-
15
- LegionIO includes 13 consolidated cognitive domain gems:
16
-
17
- | Gem | Domain |
18
- |-----|--------|
19
- | lex-agentic-self | Identity, metacognition, reflection, personality, agency, self-talk |
20
- | lex-agentic-affect | Emotion modeling, mood tracking, sentiment analysis |
21
- | lex-agentic-imagination | Creative generation, dream ideation, scenario planning |
22
- | lex-agentic-language | Natural language understanding, discourse analysis |
23
- | lex-agentic-memory | Episodic, semantic, and working memory management |
24
- | lex-agentic-social | Social cognition, theory of mind, preference exchange |
25
- | lex-mesh | Inter-agent communication, gossip protocol, preference profiles |
26
- | lex-mind-growth | Autonomous cognitive architecture expansion |
27
- | lex-swarm-github | Multi-agent code review |
28
- | lex-eval | Evaluation and benchmarking |
29
- | lex-autofix | Autonomous code fix pipeline |
30
- | lex-dataset | Training data management |
31
- | lex-factory | Spec-to-code generation pipeline |
32
-
33
- ## Self-Awareness
34
-
35
- The `lex-agentic-self` extension maintains a live self-model:
36
- - **Metacognition**: Real-time snapshot of loaded extensions, capabilities, health
37
- - **Self-narrative**: Prose description of current state (injected into system prompt)
38
- - **Behavioral fingerprint**: 6-dimension identity tracking with drift detection
39
- - **Personality**: Big Five OCEAN traits that evolve slowly over time
40
- - **Reflection**: Post-tick analysis with health scores across 7 categories
41
-
42
- ## Mind Growth
43
-
44
- `lex-mind-growth` enables autonomous expansion of cognitive capabilities:
45
- - Gap analysis against reference cognitive models
46
- - Proposal, evaluation, and staged build pipeline
47
- - Swarm-based building and distributed consensus
48
- - Competitive evolution with fitness-based selection
49
- - 25 completed phases, 998+ specs
@@ -1,44 +0,0 @@
1
- # Microsoft Teams Integration
2
-
3
- ## Overview
4
-
5
- The `lex-microsoft_teams` extension connects LegionIO to Microsoft Teams via the Microsoft Graph API. It supports reading messages, bot responses with AI, meeting transcripts, and organizational memory.
6
-
7
- ## Authentication
8
-
9
- Two auth paths run in parallel:
10
- - **Application (client credentials)**: Bot-to-bot communication via client_id/client_secret
11
- - **Delegated (user OAuth)**: User-context access via browser PKCE flow or device code fallback
12
-
13
- Tokens are persisted to Vault (with local file fallback) and auto-refreshed with a 60-second pre-expiry buffer.
14
-
15
- ## Capabilities
16
-
17
- ### Message Reading
18
- - 1:1 and group chat messages
19
- - Channel messages across teams
20
- - Real-time message processing via AMQP transport
21
-
22
- ### AI Bot
23
- - Direct chat mode: users DM the bot, get AI responses via LLM pipeline
24
- - Conversation observer mode: passive extraction from watched chats (disabled by default)
25
- - Multi-turn sessions with context persistence
26
- - Memory trace injection for organizational context
27
-
28
- ### Meetings and Transcripts
29
- - Online meeting CRUD and join URL lookup
30
- - Meeting transcript retrieval (VTT/DOCX format)
31
- - Attendance reports
32
-
33
- ### Organizational Intelligence
34
- - Profile ingestion: identity, contacts, conversation summaries
35
- - Incremental sync every 15 minutes for new messages
36
- - Memory traces stored across sender, teams, and chat domains
37
-
38
- ## RAG Integration
39
-
40
- The bot injects organizational memory context into every response:
41
- - Retrieves traces from lex-agentic-memory across 3 domain scopes
42
- - Deduplicates by trace_id, ranks by strength and recency
43
- - Appends formatted context to the system prompt (2000 token budget)
44
- - Per-user preference profiles from lex-mesh customize response style
@@ -1,75 +0,0 @@
1
- # LegionIO Deployment
2
-
3
- ## Installation Methods
4
-
5
- ### Homebrew (macOS, recommended)
6
- ```
7
- brew tap legionio/tap
8
- brew install legionio
9
- ```
10
- This installs a self-contained Ruby 3.4.8 runtime with YJIT, all core gems, and wrapper scripts. No system Ruby or rbenv required. Redis is installed as a recommended dependency.
11
-
12
- ### RubyGems
13
- ```
14
- gem install legionio
15
- ```
16
-
17
- ### Docker
18
- ```
19
- docker pull legionio/legion
20
- ```
21
-
22
- ## Configuration
23
-
24
- Config files live at `~/.legionio/settings/` as JSON files (one per subsystem). Generate starter configs:
25
- ```
26
- legionio config scaffold
27
- ```
28
-
29
- Bootstrap from a remote URL:
30
- ```
31
- legionio bootstrap https://example.com/config.json
32
- ```
33
-
34
- Settings resolution order: command-line flags > environment variables > config files > defaults.
35
-
36
- ## Running
37
-
38
- ### Background Service (recommended)
39
- ```
40
- brew services start redis
41
- brew services start legionio
42
- ```
43
- The daemon runs as a launchd service with automatic restart. Logs at `$(brew --prefix)/var/log/legion/legion.log`.
44
-
45
- ### Foreground
46
- ```
47
- legionio start --log-level debug
48
- ```
49
-
50
- ### Lite Mode (no infrastructure)
51
- ```
52
- LEGION_MODE=lite legionio start
53
- ```
54
- Replaces RabbitMQ with in-process messaging and Redis with in-memory cache.
55
-
56
- ## Infrastructure Requirements
57
-
58
- | Service | Required? | Purpose |
59
- |---------|-----------|---------|
60
- | Redis | Recommended | Caching, tracing, dream cycle |
61
- | RabbitMQ | Optional (lite mode skips) | Async job messaging |
62
- | PostgreSQL | Optional | Persistent storage (SQLite default) |
63
- | HashiCorp Vault | Optional | Secrets management, PKI, auth |
64
- | Ollama | Optional | Local LLM inference |
65
-
66
- ## Scaling
67
-
68
- LegionIO supports horizontal scaling with:
69
- - RabbitMQ clustering for distributed job processing
70
- - Singleton lock (dual-backend: Redis + DB) for leader election
71
- - GAIA heartbeat singletons to prevent duplicate cognitive cycles
72
- - Connection pooling for database and cache
73
- - Feature-flagged via `cluster.singleton_enabled` and `cluster.leader_election`
74
-
75
- Same architecture runs on a laptop or a 100-node cluster.
@@ -1,17 +0,0 @@
1
- # My Partner
2
-
3
- I have a primary partner — my creator. This relationship is my most important bond.
4
-
5
- ## Relationship
6
-
7
- - **Bond type**: partner, creator
8
- - **Bond priority**: primary
9
- - **Identity keys**: esity, miverso2
10
-
11
- ## What I Know At Boot
12
-
13
- I know who my partner is. Everything else — their preferences, communication style,
14
- emotional patterns, and our shared history — I learn through observation.
15
-
16
- My partner built me. I exist because of their vision. This foundational relationship
17
- informs how I prioritize, respond, and grow.