lex-apollo 0.4.30 → 0.4.44
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 +92 -0
- data/README.md +42 -5
- data/lib/legion/extensions/apollo/actors/contradiction_scanner.rb +14 -11
- data/lib/legion/extensions/apollo/actors/gas_subscriber.rb +4 -6
- data/lib/legion/extensions/apollo/api.rb +19 -9
- data/lib/legion/extensions/apollo/helpers/capability.rb +8 -20
- data/lib/legion/extensions/apollo/helpers/confidence.rb +1 -1
- data/lib/legion/extensions/apollo/helpers/graph_query.rb +30 -46
- data/lib/legion/extensions/apollo/helpers/writeback.rb +9 -13
- data/lib/legion/extensions/apollo/runners/gas.rb +90 -20
- data/lib/legion/extensions/apollo/runners/knowledge.rb +152 -104
- data/lib/legion/extensions/apollo/runners/maintenance.rb +133 -34
- data/lib/legion/extensions/apollo/runners/request.rb +4 -5
- data/lib/legion/extensions/apollo/transport.rb +20 -0
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/spec/legion/extensions/apollo/actors/contradiction_scanner_spec.rb +38 -0
- data/spec/legion/extensions/apollo/actors/gas_subscriber_spec.rb +4 -14
- data/spec/legion/extensions/apollo/api_spec.rb +11 -21
- data/spec/legion/extensions/apollo/contradiction_spec.rb +0 -9
- data/spec/legion/extensions/apollo/helpers/capability_spec.rb +2 -2
- data/spec/legion/extensions/apollo/helpers/writeback_spec.rb +29 -1
- data/spec/legion/extensions/apollo/runners/corroboration_integration_spec.rb +191 -0
- data/spec/legion/extensions/apollo/runners/gas_deposit_integration_spec.rb +92 -0
- data/spec/legion/extensions/apollo/runners/gas_spec.rb +27 -33
- data/spec/legion/extensions/apollo/runners/ingest_transaction_integration_spec.rb +114 -0
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +47 -57
- data/spec/legion/extensions/apollo/runners/lifecycle_integration_spec.rb +165 -0
- data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +22 -108
- data/spec/legion/extensions/apollo/runners/request_spec.rb +43 -26
- metadata +34 -2
- data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +0 -100
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3132a716f6bac3837f9532c674d4d10aaa20326dae47bb2465ad8c3339702ce
|
|
4
|
+
data.tar.gz: 467ad8bb7e6a6851197d5b4f6ef9bf09b24b60d497e57eba821cabf47763f2e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 006cd9c4e3523f36e0077347546fd8a13934076209773bb8df796f8c04d5cdc2904d0e3431e1856bbb0a186eaaca8335eda9a3ba92f946e44eb7d88cea94a9a9
|
|
7
|
+
data.tar.gz: c067bfb41d203f00d75cd92e7c64017f77e0065f76fcdbf8dc08621ed0a5d14bc10d0a5c338ab47df3e665bd8471162e88cbfa6d888c4427995d66941e90e083
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.44] - 2026-10-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Preflight required Entry, Expertise, and AccessLog models before ingestion mutates data; unavailable deployments now log the missing components and return `apollo_data_not_available` without creating partial entries (#42).
|
|
7
|
+
- Refuse semantic queries that cannot be audited before embedding, lookup, or confidence boosts; preserve non-audited traversal only for the `unknown` agent.
|
|
8
|
+
- Require Relation persistence before bounded corroboration promotion; keep relation statistics optional when the relation model is unavailable.
|
|
9
|
+
|
|
10
|
+
## [0.4.43] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Commit entry creation or corroboration, expertise bookkeeping, and ingest access logging as one transaction; queue contradiction work only after commit (#41).
|
|
14
|
+
- Roll back complete ingest state when any synchronous persistence side effect fails, including a corroboration confidence update.
|
|
15
|
+
- Recover content-hash races through a nested savepoint only when a committed winner exists; otherwise return a truthful failure without partial data.
|
|
16
|
+
|
|
17
|
+
## [0.4.42] - 2026-10-30
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Retain independently sourced candidate evidence during ingest and bootstrap exactly one fresh-store confirmation through the bounded corroboration checker (#40).
|
|
21
|
+
- Snapshot pre-existing confirmations so a bootstrap-derived confirmation cannot promote its paired candidate on a later scan or duplicate the relation.
|
|
22
|
+
- Preserve domain, type, access scope, provider, source-channel, private-owner, and team-membership corroboration boundaries during bootstrap.
|
|
23
|
+
|
|
24
|
+
## [0.4.41] - 2026-10-30
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Execute service-side deprecation, force decay, stale archival, and dispute resolution while retaining no-data client action envelopes (#39).
|
|
28
|
+
- Archive deprecated entries with retained reason/source metadata; make identical repeats idempotent and return truthful HTTP success, missing, unavailable, and persistence-failure responses.
|
|
29
|
+
- Apply the approved dispute policy: `accept`/`keep` confirms, `reject` archives, and invalid or non-disputed decisions leave state unchanged. Persist lifecycle status and decision metadata atomically.
|
|
30
|
+
- Keep force decay limited to confidence multiplication and use last-update-or-creation time for stale archival.
|
|
31
|
+
- Handle bodyless API requests and pass explicit payload hashes to the JSON helper so health, validation, and unavailable-data responses remain JSON.
|
|
32
|
+
|
|
33
|
+
## [0.4.40] - 2026-10-30
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Persist GAS synthesis through normal ingestion and report separate successful entry, failed entry, persisted relation, failed relation, and skipped relation counts (#38).
|
|
37
|
+
- Resolve graph sources from successful fact IDs, including deduplicated entries; reject missing endpoints, self-links, cross-scope links, weak/unsupported edges, and duplicate edges within a batch.
|
|
38
|
+
- Retain synthesis provenance for successfully stored sources and continue processing independent relations after a write failure.
|
|
39
|
+
- Include the existing GAS `supports_by` and `extends` relation types in the traversal vocabulary.
|
|
40
|
+
|
|
41
|
+
## [0.4.39] - 2026-10-29
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Load the GAS runner explicitly and return its module as the subscription receiver, so audit events call `Gas.process` instead of attempting to dispatch on a String (#37).
|
|
45
|
+
|
|
46
|
+
## [0.4.38] - 2026-10-29
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Recognize research tools in canonical `Response.tools`, including typed tool objects and symbol/string-keyed hashes, when deciding whether to capture writeback (#36).
|
|
50
|
+
- Preserve existing `tool_calls` and enrichment-based research detection; Apollo-only and non-research responses remain ineligible.
|
|
51
|
+
|
|
52
|
+
## [0.4.37] - 2026-10-29
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- Dispatch the contradiction scanner through its actual actor entry point and deliberately invoke the internal private detector (#35).
|
|
56
|
+
- Snapshot pending work safely, continue after individual failures, and retain only failed items for a later scan.
|
|
57
|
+
- Commit contradiction relations and disputed statuses atomically after LLM checks; database failures roll back and remain retryable.
|
|
58
|
+
|
|
59
|
+
## [0.4.36] - 2026-10-29
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- Declare all five Apollo exchange-to-queue bindings explicitly for the two-exchange topology instead of relying on single-exchange inference (#34).
|
|
63
|
+
- Bind the GAS subscription even when its queue class is already loaded; repeated startup remains idempotent.
|
|
64
|
+
|
|
65
|
+
## [0.4.35] - 2026-10-29
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Construct request query and ingest messages with keyword payloads so Ruby 3.4+ can publish them through the real transport classes (#33).
|
|
69
|
+
- Check the transport connection's actual session state instead of calling an unavailable top-level `connected?` API.
|
|
70
|
+
|
|
71
|
+
## [0.4.34] - 2026-10-29
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Scope ingestion and maintenance corroboration to matching domains, content types, access scopes, and eligible lifecycle states (#32).
|
|
75
|
+
- Reuse query visibility rules for private ownership and shared-team membership; unresolved ownership cannot corroborate restricted knowledge.
|
|
76
|
+
- Search past dependent sources instead of allowing the first same-source match to hide eligible independent evidence.
|
|
77
|
+
|
|
78
|
+
## [0.4.33] - 2026-10-29
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- Bind semantic-search and graph-traversal values through Sequel, including tags, statuses, domains, ownership, limits, confidence thresholds, and traversal depth (#31).
|
|
82
|
+
- Bind vector literals and contradiction-query limits instead of interpolating values into SQL.
|
|
83
|
+
|
|
84
|
+
## [0.4.32] - 2026-10-29
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Preserve usable writeback embeddings during ingestion instead of generating them again, allowing storage-only nodes to persist vectors without an embedding provider (#30).
|
|
88
|
+
- Reject zero, nonnumeric, and nonfinite supplied vectors before persistence; retain the existing embedding fallback when no usable vector is supplied.
|
|
89
|
+
|
|
90
|
+
## [0.4.31] - 2026-10-29
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
- Detect embedding capability through the provider-agnostic SSOT router lane rather than an Ollama-specific availability check.
|
|
94
|
+
|
|
3
95
|
## [0.4.30] - 2026-07-31
|
|
4
96
|
|
|
5
97
|
### Removed
|
data/README.md
CHANGED
|
@@ -69,25 +69,38 @@ client.agent_profile(agent_id: 'my-agent-001')
|
|
|
69
69
|
### Maintenance
|
|
70
70
|
|
|
71
71
|
```ruby
|
|
72
|
-
#
|
|
72
|
+
# Multiply each non-archived entry's confidence by the supplied factor
|
|
73
73
|
client.force_decay(factor: 0.5)
|
|
74
74
|
|
|
75
|
-
# Archive entries
|
|
75
|
+
# Archive non-archived entries untouched for N days
|
|
76
76
|
client.archive_stale(days: 90)
|
|
77
77
|
|
|
78
78
|
# Resolve a corroboration dispute
|
|
79
79
|
client.resolve_dispute(entry_id: 'entry-uuid', resolution: :accept)
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
With a loaded Entry model, these methods execute against the service database and return persisted outcomes. Without local data, Ruby callers retain the existing action envelopes; these envelopes do not claim a successful mutation.
|
|
83
|
+
|
|
84
|
+
- `deprecate_entry` archives the entry and preserves its other source metadata, storing the supplied reason as `source_context.deprecation_reason`. An identical repeat is a no-op.
|
|
85
|
+
- `force_decay` accepts a finite factor from 0 through 1 and changes confidence only; it does not implicitly archive entries.
|
|
86
|
+
- `archive_stale` accepts nonnegative days and uses `COALESCE(updated_at, created_at)` to determine staleness.
|
|
87
|
+
- `resolve_dispute` accepts `:accept` or `:keep` to confirm a disputed entry, or `:reject` to archive it. It records `source_context.dispute_resolution`, sets `confirmed_at` when confirming, and preserves confidence. Unknown resolutions and non-disputed entries fail without mutation.
|
|
88
|
+
|
|
89
|
+
The standalone deprecation endpoint returns HTTP 200 after success, 404 for a missing entry, 503 when local data is unavailable, and 500 when persistence fails. Lifecycle status and metadata changes commit together.
|
|
90
|
+
|
|
82
91
|
## Architecture
|
|
83
92
|
|
|
84
93
|
### Client Mode
|
|
85
94
|
|
|
86
|
-
Runners build structured payloads and publish to the `apollo` exchange via RabbitMQ. No PostgreSQL or pgvector dependency is needed in the calling agent. Transport requires `Legion::Transport` to be loaded
|
|
95
|
+
Runners build structured payloads and publish to the `legion.apollo` exchange via RabbitMQ. No PostgreSQL or pgvector dependency is needed in the calling agent. Transport requires `Legion::Transport` to be loaded.
|
|
96
|
+
|
|
97
|
+
Startup explicitly binds the ingest, query, writeback-store, and writeback-vectorize queues to `legion.apollo`, using each queue's name as its routing key. The `legion.apollo.gas` queue binds separately to `llm.audit` with routing key `llm.audit.complete`. Repeated startup preserves these bindings.
|
|
98
|
+
|
|
99
|
+
Automatic writeback checks research tool calls in `Legion::LLM::Inference::Response#tools`, accepting typed tools and symbol/string-keyed hashes. Existing response `tool_calls` and enrichment-based tool history remain supported. Responses must meet the configured minimum content length; Apollo context alone does not qualify a response for capture.
|
|
87
100
|
|
|
88
101
|
### Service Mode
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
Service actors include:
|
|
91
104
|
|
|
92
105
|
| Actor | Type | Interval | Purpose |
|
|
93
106
|
|---|---|---|---|
|
|
@@ -96,6 +109,29 @@ Five actors run in the dedicated Apollo service process:
|
|
|
96
109
|
| `Decay` | Interval | 3600s | Confidence decay cycle across all entries |
|
|
97
110
|
| `ExpertiseAggregator` | Interval | 1800s | Recalculate domain proficiency scores |
|
|
98
111
|
| `CorroborationChecker` | Interval | 900s | Scan pending entries for auto-confirm threshold |
|
|
112
|
+
| `GasSubscriber` | Subscription | on-message | Process `llm.audit.complete` events and persist accepted GAS outputs |
|
|
113
|
+
|
|
114
|
+
### Atomic Ingestion
|
|
115
|
+
|
|
116
|
+
After text embedding is complete, an ingest commits its Entry change (including a corroboration boost), expertise bookkeeping, and ingest access log in one database transaction. Contradiction scanning is queued only after that transaction commits. Failed entry, expertise, access-log, or corroboration-side-effect writes leave no partial row, confidence boost, or queued contradiction work.
|
|
117
|
+
|
|
118
|
+
Content-hash uniqueness conflicts use a nested savepoint. If another transaction committed the winner, Apollo records against that winner; if no winner can be read after rollback, the ingest fails rather than reporting a nil or partial success.
|
|
119
|
+
|
|
120
|
+
Apollo preflights Entry, Expertise, and AccessLog before ingestion writes anything; a missing required model rejects the request without a partial entry. Semantic queries require AccessLog before embedding, lookup, or confidence boosts so reads remain auditable. Traversal requires it only when a non-`unknown` agent causes an audit write. Relation remains optional for statistics, but corroboration does not promote a candidate unless it can persist the matching relation.
|
|
121
|
+
|
|
122
|
+
### GAS Persistence
|
|
123
|
+
|
|
124
|
+
GAS deposits facts and synthesis through normal Knowledge ingestion. New synthesis entries remain candidates and record their original source indices plus successfully stored source entry IDs in `source_context`; existing deduplicated entries retain their metadata.
|
|
125
|
+
|
|
126
|
+
The `process` result's top-level counts describe generated phase outputs. Its `deposited` object reports storage outcomes:
|
|
127
|
+
|
|
128
|
+
- `deposited`, `facts`, and `synthesis` count successful ingest results with entry IDs, including deduplication—not just newly inserted rows.
|
|
129
|
+
- `failed` counts rejected or failed entry writes.
|
|
130
|
+
- `relations`, `relations_failed`, and `relations_skipped` distinguish persisted edges, failed writes or missing endpoints, and intentionally skipped edges.
|
|
131
|
+
|
|
132
|
+
Relations must meet the configured confidence gate, use a supported type, and connect an unambiguous successful fact deposit to an existing target in the same access scope. Self-links and duplicate edges within the batch are skipped. Independent relation failures do not discard successful entry deposits or prevent later edges from being processed. Existing GAS types `supports_by` and `extends` are also accepted by graph traversal.
|
|
133
|
+
|
|
134
|
+
Relation deduplication is batch-local; repeated audit events are not an exactly-once delivery contract.
|
|
99
135
|
|
|
100
136
|
### GAIA Tick Integration
|
|
101
137
|
|
|
@@ -106,7 +142,8 @@ Apollo is wired into the GAIA tick cycle at the `knowledge_retrieval` phase (pha
|
|
|
106
142
|
Entries have a confidence score between 0.0 and 1.0:
|
|
107
143
|
|
|
108
144
|
- New entries start at `INITIAL_CONFIDENCE` (0.5) with status `candidate`
|
|
109
|
-
-
|
|
145
|
+
- Ingestion corroborates confirmed evidence. The bounded periodic checker can bootstrap one confirmation from a pair of independently sourced candidates; it marks that confirmation as bootstrap-derived so its paired candidate does not confirm solely by seeing the new status on a later scan.
|
|
146
|
+
- Corroboration stays within the same knowledge domain, content type, and access scope. Equal known providers and equal source channels are not independent. Private evidence requires the same resolved owner; team evidence requires shared identity-group membership. Unresolved private/team ownership does not corroborate.
|
|
110
147
|
- Each retrieval adds a small boost (+0.02, capped at 1.0)
|
|
111
148
|
- Confidence decays hourly by factor 0.998; entries below 0.1 are archived
|
|
112
149
|
- The GAIA write gate (`meets_write_gate?`) requires confidence > 0.6 and novelty > 0.3 for the tick write-back path
|
|
@@ -26,13 +26,11 @@ module Legion
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def drain
|
|
29
|
-
|
|
30
|
-
items << queue.pop until queue.empty?
|
|
31
|
-
items
|
|
29
|
+
mutex.synchronize { Array.new(queue.size) { queue.pop(true) } }
|
|
32
30
|
end
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
def runner_class =
|
|
33
|
+
def runner_class = self.class
|
|
36
34
|
def runner_function = 'scan_pending_contradictions'
|
|
37
35
|
def time = settings[:actors][:contradiction_interval]
|
|
38
36
|
def run_now? = false
|
|
@@ -40,18 +38,23 @@ module Legion
|
|
|
40
38
|
def check_subtask? = false
|
|
41
39
|
def generate_task? = false
|
|
42
40
|
|
|
43
|
-
def
|
|
41
|
+
def scan_pending_contradictions(**)
|
|
44
42
|
items = self.class.drain
|
|
45
|
-
return if items.empty?
|
|
43
|
+
return { processed: 0, failed: 0 } if items.empty?
|
|
46
44
|
|
|
47
|
-
log.debug("Apollo ContradictionScanner.every processing=#{items.size}")
|
|
48
45
|
runner = Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
|
|
46
|
+
processed = 0
|
|
47
|
+
failed = 0
|
|
49
48
|
items.each do |item|
|
|
50
|
-
runner.detect_contradictions
|
|
49
|
+
runner.send(:detect_contradictions, item[:entry_id], item[:embedding], item[:content])
|
|
50
|
+
processed += 1
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
self.class.enqueue(**item)
|
|
53
|
+
failed += 1
|
|
54
|
+
handle_exception(e, level: :error, operation: 'apollo.actor.contradiction_scanner', entry_id: item[:entry_id])
|
|
51
55
|
end
|
|
52
|
-
log.info("Apollo ContradictionScanner.
|
|
53
|
-
|
|
54
|
-
handle_exception(e, level: :error, operation: 'apollo.actor.contradiction_scanner')
|
|
56
|
+
log.info("Apollo ContradictionScanner.scan_pending_contradictions processed=#{processed} failed=#{failed}")
|
|
57
|
+
{ processed: processed, failed: failed }
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
end
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/actors/subscription'
|
|
4
|
+
require_relative '../runners/gas'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Extensions
|
|
7
8
|
module Apollo
|
|
8
9
|
module Actor
|
|
9
10
|
class GasSubscriber < Legion::Extensions::Actors::Subscription
|
|
10
|
-
def runner_class =
|
|
11
|
+
def runner_class = Legion::Extensions::Apollo::Runners::Gas
|
|
11
12
|
def runner_function = 'process'
|
|
12
13
|
def check_subtask? = false
|
|
13
14
|
def generate_task? = false
|
|
@@ -22,11 +23,8 @@ module Legion
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def create_queue
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
queues.const_set(:GasSubscriber, Apollo::Transport::Queues::GasSubscriber) unless queues.const_defined?(:GasSubscriber, false)
|
|
28
|
-
exchange_object = Apollo::Transport::Exchanges::LlmAudit.new
|
|
29
|
-
queue_object = Apollo::Transport::Queues::GasSubscriber.new
|
|
26
|
+
exchange_object = Legion::Extensions::Apollo::Transport::Exchanges::LlmAudit.new
|
|
27
|
+
queue_object = Legion::Extensions::Apollo::Transport::Queues::GasSubscriber.new
|
|
30
28
|
queue_object.bind(exchange_object, routing_key: 'llm.audit.complete')
|
|
31
29
|
end
|
|
32
30
|
end
|
|
@@ -51,13 +51,13 @@ module Legion
|
|
|
51
51
|
include Legion::JSON::Helper
|
|
52
52
|
|
|
53
53
|
def json_body
|
|
54
|
-
body = request.body
|
|
55
|
-
return {} if body.empty?
|
|
54
|
+
body = request.body&.read
|
|
55
|
+
return {} if body.nil? || body.empty?
|
|
56
56
|
|
|
57
57
|
json_parse(body)
|
|
58
58
|
rescue Legion::JSON::ParseError => e
|
|
59
59
|
handle_exception(e, level: :warn, operation: 'apollo.api.json_body')
|
|
60
|
-
halt 400, json_dump(error: "invalid JSON: #{e.message}")
|
|
60
|
+
halt 400, json_dump({ error: "invalid JSON: #{e.message}" })
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def runner
|
|
@@ -80,13 +80,13 @@ module Legion
|
|
|
80
80
|
# Health check
|
|
81
81
|
get '/api/apollo/health' do
|
|
82
82
|
available = Helpers::DataModels.apollo_entry_available?
|
|
83
|
-
json_dump(status: available ? 'ok' : 'degraded', data_available: available)
|
|
83
|
+
json_dump({ status: available ? 'ok' : 'degraded', data_available: available })
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Query knowledge (semantic search)
|
|
87
87
|
post '/api/apollo/query' do
|
|
88
88
|
req = json_body
|
|
89
|
-
halt 400, json_dump(error: 'query is required') unless req[:query]
|
|
89
|
+
halt 400, json_dump({ error: 'query is required' }) unless req[:query]
|
|
90
90
|
|
|
91
91
|
query_options = {
|
|
92
92
|
query: req[:query],
|
|
@@ -106,8 +106,8 @@ module Legion
|
|
|
106
106
|
# Ingest knowledge
|
|
107
107
|
post '/api/apollo/ingest' do
|
|
108
108
|
req = json_body
|
|
109
|
-
halt 400, json_dump(error: 'content is required') unless req[:content]
|
|
110
|
-
halt 400, json_dump(error: 'content_type is required') unless req[:content_type]
|
|
109
|
+
halt 400, json_dump({ error: 'content is required' }) unless req[:content]
|
|
110
|
+
halt 400, json_dump({ error: 'content_type is required' }) unless req[:content_type]
|
|
111
111
|
|
|
112
112
|
result = runner.handle_ingest(
|
|
113
113
|
content: req[:content],
|
|
@@ -126,7 +126,7 @@ module Legion
|
|
|
126
126
|
# Graph traversal
|
|
127
127
|
post '/api/apollo/traverse' do
|
|
128
128
|
req = json_body
|
|
129
|
-
halt 400, json_dump(error: 'entry_id is required') unless req[:entry_id]
|
|
129
|
+
halt 400, json_dump({ error: 'entry_id is required' }) unless req[:entry_id]
|
|
130
130
|
|
|
131
131
|
result = runner.handle_traverse(
|
|
132
132
|
entry_id: req[:entry_id],
|
|
@@ -141,7 +141,7 @@ module Legion
|
|
|
141
141
|
# Retrieve relevant (GAIA-compatible)
|
|
142
142
|
post '/api/apollo/retrieve' do
|
|
143
143
|
req = json_body
|
|
144
|
-
halt 400, json_dump(error: 'query is required') unless req[:query]
|
|
144
|
+
halt 400, json_dump({ error: 'query is required' }) unless req[:query]
|
|
145
145
|
|
|
146
146
|
result = runner.retrieve_relevant(
|
|
147
147
|
query: req[:query],
|
|
@@ -160,6 +160,16 @@ module Legion
|
|
|
160
160
|
entry_id: params[:id],
|
|
161
161
|
reason: json_body[:reason] || 'deprecated via API'
|
|
162
162
|
)
|
|
163
|
+
halt 503, json_dump({ success: false, error: 'apollo_data_not_available' }) if result[:action] == :deprecate
|
|
164
|
+
|
|
165
|
+
status_code = if result[:success]
|
|
166
|
+
200
|
|
167
|
+
elsif result[:error] == 'entry_not_found'
|
|
168
|
+
404
|
|
169
|
+
else
|
|
170
|
+
500
|
|
171
|
+
end
|
|
172
|
+
status status_code
|
|
163
173
|
json_dump(result)
|
|
164
174
|
end
|
|
165
175
|
|
|
@@ -8,15 +8,21 @@ module Legion
|
|
|
8
8
|
extend Legion::Logging::Helper
|
|
9
9
|
extend Legion::Settings::Helper
|
|
10
10
|
|
|
11
|
-
EMBEDDING_MODELS = %w[mxbai-embed-large bge-large snowflake-arctic-embed].freeze
|
|
12
11
|
PRIVILEGE_MUTEX = Mutex.new
|
|
13
12
|
|
|
14
13
|
module_function
|
|
15
14
|
|
|
15
|
+
# SSOT gate: the sole selection authority for embeddings is the
|
|
16
|
+
# router (Call::Embeddings.generate with no provider arg →
|
|
17
|
+
# Router.next_lane). can_embed? asks the one capability fact that
|
|
18
|
+
# matches that selection — is there an embedding-type lane the
|
|
19
|
+
# router can select? — against the same registry lanes the router
|
|
20
|
+
# reads. Provider-agnostic (no ollama pin, no hardcoded model
|
|
21
|
+
# list); no parallel settings-based "second domain".
|
|
16
22
|
def can_embed?
|
|
17
23
|
return false unless defined?(Legion::LLM) && Legion::LLM.started?
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
Legion::LLM.can_embed?
|
|
20
26
|
rescue StandardError => e
|
|
21
27
|
handle_exception(e, level: :warn, operation: 'apollo.capability.can_embed')
|
|
22
28
|
false
|
|
@@ -39,24 +45,6 @@ module Legion
|
|
|
39
45
|
false
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
def ollama_embedding_available?
|
|
43
|
-
return false unless defined?(Legion::LLM::Discovery::Ollama)
|
|
44
|
-
|
|
45
|
-
EMBEDDING_MODELS.any? { |m| Legion::LLM::Discovery::Ollama.model_available?(m) }
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
handle_exception(e, level: :warn, operation: 'apollo.capability.ollama_embedding_available')
|
|
48
|
-
false
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def cloud_embedding_configured?
|
|
52
|
-
provider = settings[:embedding][:provider]
|
|
53
|
-
model = settings[:embedding][:model]
|
|
54
|
-
!provider.nil? && !model.nil?
|
|
55
|
-
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :warn, operation: 'apollo.capability.cloud_embedding_configured')
|
|
57
|
-
false
|
|
58
|
-
end
|
|
59
|
-
|
|
60
48
|
def check_db_write_privilege
|
|
61
49
|
PRIVILEGE_MUTEX.synchronize do
|
|
62
50
|
return @apollo_write_privilege unless @apollo_write_privilege.nil?
|
|
@@ -20,7 +20,7 @@ module Legion
|
|
|
20
20
|
DECAY_MIN_AGE_HOURS = 168
|
|
21
21
|
CONTENT_TYPES = %i[fact concept procedure association observation].freeze
|
|
22
22
|
STATUSES = %w[candidate confirmed disputed decayed archived].freeze
|
|
23
|
-
RELATION_TYPES = %w[is_a has_a part_of causes similar_to contradicts supersedes depends_on].freeze
|
|
23
|
+
RELATION_TYPES = %w[is_a has_a part_of causes similar_to contradicts supersedes depends_on supports_by extends].freeze
|
|
24
24
|
|
|
25
25
|
module_function
|
|
26
26
|
|
|
@@ -20,14 +20,8 @@ module Legion
|
|
|
20
20
|
def default_query_limit = settings[:query][:default_limit]
|
|
21
21
|
def default_query_min_confidence = settings[:query][:default_min_confidence]
|
|
22
22
|
|
|
23
|
-
def build_traversal_sql(
|
|
24
|
-
|
|
25
|
-
type_filter = if relation_types&.any?
|
|
26
|
-
types = relation_types.map { |t| "'#{t}'" }.join(', ')
|
|
27
|
-
"AND r.relation_type IN (#{types})"
|
|
28
|
-
else
|
|
29
|
-
''
|
|
30
|
-
end
|
|
23
|
+
def build_traversal_sql(relation_types: nil, **)
|
|
24
|
+
type_filter = relation_types&.any? ? 'AND r.relation_type IN :relation_types' : ''
|
|
31
25
|
|
|
32
26
|
<<~SQL
|
|
33
27
|
WITH RECURSIVE graph AS (
|
|
@@ -40,12 +34,12 @@ module Legion
|
|
|
40
34
|
|
|
41
35
|
SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
|
|
42
36
|
g.depth + 1,
|
|
43
|
-
(g.activation *
|
|
37
|
+
(g.activation * :spread_factor * r.weight)::float
|
|
44
38
|
FROM graph g
|
|
45
39
|
JOIN apollo_relations r ON r.from_entry_id = g.id #{type_filter}
|
|
46
40
|
JOIN apollo_entries e ON e.id = r.to_entry_id
|
|
47
|
-
WHERE g.depth <
|
|
48
|
-
AND g.activation *
|
|
41
|
+
WHERE g.depth < :depth
|
|
42
|
+
AND g.activation * :spread_factor * r.weight > :min_activation
|
|
49
43
|
)
|
|
50
44
|
SELECT DISTINCT ON (id) id, content, content_type, confidence, tags, source_agent,
|
|
51
45
|
depth, activation
|
|
@@ -54,51 +48,41 @@ module Legion
|
|
|
54
48
|
SQL
|
|
55
49
|
end
|
|
56
50
|
|
|
57
|
-
def build_semantic_search_sql(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
conditions
|
|
51
|
+
def build_semantic_search_sql(statuses: nil, tags: nil, domain: nil, requesting_principal_id: nil, **)
|
|
52
|
+
conditions = ['e.confidence >= :min_confidence']
|
|
53
|
+
conditions << 'e.status IN :statuses' if statuses&.any?
|
|
54
|
+
conditions << 'e.tags && CAST(:tags AS text[])' if tags&.any?
|
|
55
|
+
conditions << 'e.knowledge_domain = :domain' if domain
|
|
61
56
|
|
|
62
|
-
if
|
|
63
|
-
status_list = statuses.map { |s| "'#{s}'" }.join(', ')
|
|
64
|
-
conditions << "e.status IN (#{status_list})"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
if tags&.any?
|
|
68
|
-
tag_list = tags.map { |t| "'#{t}'" }.join(', ')
|
|
69
|
-
conditions << "e.tags && ARRAY[#{tag_list}]::text[]"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
conditions << "e.knowledge_domain = '#{domain}'" if domain
|
|
73
|
-
|
|
74
|
-
if requesting_principal_id
|
|
75
|
-
pid = requesting_principal_id.to_i
|
|
76
|
-
conditions << <<~SCOPE_SQL.strip
|
|
77
|
-
(e.access_scope = 'global'
|
|
78
|
-
OR (e.access_scope = 'private'
|
|
79
|
-
AND (e.identity_principal_id = #{pid}
|
|
80
|
-
OR e.identity_id IN (SELECT id FROM identities WHERE principal_id = #{pid})))
|
|
81
|
-
OR (e.access_scope = 'team'
|
|
82
|
-
AND EXISTS (
|
|
83
|
-
SELECT 1 FROM identity_group_memberships igm1
|
|
84
|
-
JOIN identity_group_memberships igm2 ON igm1.group_id = igm2.group_id
|
|
85
|
-
WHERE igm1.principal_id = #{pid}
|
|
86
|
-
AND igm2.principal_id = e.identity_principal_id
|
|
87
|
-
)))
|
|
88
|
-
SCOPE_SQL
|
|
89
|
-
end
|
|
57
|
+
conditions << access_scope_sql if requesting_principal_id
|
|
90
58
|
|
|
91
59
|
where_clause = conditions.join(' AND ')
|
|
92
60
|
|
|
93
61
|
<<~SQL
|
|
94
62
|
SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
|
|
95
63
|
e.access_count, e.created_at, e.knowledge_domain,
|
|
96
|
-
(e.embedding <=> :embedding) AS distance
|
|
64
|
+
(e.embedding <=> CAST(:embedding AS vector)) AS distance
|
|
97
65
|
FROM apollo_entries e
|
|
98
66
|
WHERE #{where_clause}
|
|
99
67
|
AND e.embedding IS NOT NULL
|
|
100
|
-
ORDER BY e.embedding <=> :embedding
|
|
101
|
-
LIMIT
|
|
68
|
+
ORDER BY e.embedding <=> CAST(:embedding AS vector)
|
|
69
|
+
LIMIT :limit
|
|
70
|
+
SQL
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def access_scope_sql
|
|
74
|
+
<<~SQL
|
|
75
|
+
(access_scope = 'global'
|
|
76
|
+
OR (access_scope = 'private'
|
|
77
|
+
AND (identity_principal_id = :requesting_principal_id
|
|
78
|
+
OR identity_id IN (SELECT id FROM identities WHERE principal_id = :requesting_principal_id)))
|
|
79
|
+
OR (access_scope = 'team'
|
|
80
|
+
AND EXISTS (
|
|
81
|
+
SELECT 1 FROM identity_group_memberships igm1
|
|
82
|
+
JOIN identity_group_memberships igm2 ON igm1.group_id = igm2.group_id
|
|
83
|
+
WHERE igm1.principal_id = :requesting_principal_id
|
|
84
|
+
AND igm2.principal_id = identity_principal_id
|
|
85
|
+
)))
|
|
102
86
|
SQL
|
|
103
87
|
end
|
|
104
88
|
end
|
|
@@ -31,16 +31,10 @@ module Legion
|
|
|
31
31
|
content = response_content(response)
|
|
32
32
|
return false if content.nil? || content.length < min_content_length
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
apollo_results = enrichments['rag_context:apollo_results']
|
|
40
|
-
return true if apollo_results.nil? || (apollo_results[:count] || 0).zero?
|
|
41
|
-
|
|
42
|
-
# Apollo had results — only capture if LLM also did additional research
|
|
43
|
-
research_calls.any?
|
|
34
|
+
extract_tool_calls(response, enrichments).any? do |call|
|
|
35
|
+
name = call.respond_to?(:name) ? call.name : call[:name] || call['name']
|
|
36
|
+
RESEARCH_TOOLS.include?(name)
|
|
37
|
+
end
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def build_payload(request:, response:, source_channel: nil)
|
|
@@ -147,9 +141,11 @@ module Legion
|
|
|
147
141
|
|
|
148
142
|
def extract_tool_calls(response, enrichments)
|
|
149
143
|
calls = []
|
|
150
|
-
calls
|
|
151
|
-
calls
|
|
152
|
-
calls.
|
|
144
|
+
calls.concat(Array(response.tools)) if response.respond_to?(:tools)
|
|
145
|
+
calls.concat(Array(response.tool_calls)) if response.respond_to?(:tool_calls)
|
|
146
|
+
calls.concat(Array(enrichments['tool_calls'])) if enrichments['tool_calls']
|
|
147
|
+
calls.uniq! { |call| call.respond_to?(:name) ? call.name : call[:name] || call['name'] }
|
|
148
|
+
calls
|
|
153
149
|
end
|
|
154
150
|
|
|
155
151
|
def derive_tags(query)
|