lex-apollo 0.2.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +135 -0
  4. data/lib/legion/extensions/apollo/actors/corroboration_checker.rb +22 -0
  5. data/lib/legion/extensions/apollo/actors/decay.rb +22 -0
  6. data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +22 -0
  7. data/lib/legion/extensions/apollo/actors/ingest.rb +25 -0
  8. data/lib/legion/extensions/apollo/actors/query_responder.rb +25 -0
  9. data/lib/legion/extensions/apollo/client.rb +30 -0
  10. data/lib/legion/extensions/apollo/helpers/confidence.rb +46 -0
  11. data/lib/legion/extensions/apollo/helpers/embedding.rb +22 -0
  12. data/lib/legion/extensions/apollo/helpers/graph_query.rb +77 -0
  13. data/lib/legion/extensions/apollo/helpers/similarity.rb +36 -0
  14. data/lib/legion/extensions/apollo/runners/expertise.rb +71 -0
  15. data/lib/legion/extensions/apollo/runners/knowledge.rb +213 -0
  16. data/lib/legion/extensions/apollo/runners/maintenance.rb +72 -0
  17. data/lib/legion/extensions/apollo/transport/exchanges/apollo.rb +19 -0
  18. data/lib/legion/extensions/apollo/transport/messages/ingest.rb +43 -0
  19. data/lib/legion/extensions/apollo/transport/messages/query.rb +43 -0
  20. data/lib/legion/extensions/apollo/transport/queues/ingest.rb +23 -0
  21. data/lib/legion/extensions/apollo/transport/queues/query.rb +23 -0
  22. data/lib/legion/extensions/apollo/version.rb +9 -0
  23. data/lib/legion/extensions/apollo.rb +25 -0
  24. data/spec/legion/extensions/apollo/actors/decay_spec.rb +45 -0
  25. data/spec/legion/extensions/apollo/actors/expertise_aggregator_spec.rb +41 -0
  26. data/spec/legion/extensions/apollo/actors/ingest_spec.rb +33 -0
  27. data/spec/legion/extensions/apollo/client_spec.rb +75 -0
  28. data/spec/legion/extensions/apollo/helpers/confidence_spec.rb +109 -0
  29. data/spec/legion/extensions/apollo/helpers/embedding_spec.rb +68 -0
  30. data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +69 -0
  31. data/spec/legion/extensions/apollo/helpers/similarity_spec.rb +58 -0
  32. data/spec/legion/extensions/apollo/runners/expertise_spec.rb +111 -0
  33. data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +308 -0
  34. data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +133 -0
  35. data/spec/legion/extensions/apollo/transport/messages/ingest_spec.rb +65 -0
  36. data/spec/legion/extensions/apollo/transport/messages/query_spec.rb +75 -0
  37. data/spec/spec_helper.rb +30 -0
  38. metadata +108 -0
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../helpers/confidence'
5
+ require_relative '../helpers/embedding'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Apollo
10
+ module Runners
11
+ module Knowledge
12
+ def store_knowledge(content:, content_type:, tags: [], source_agent: nil, context: {}, **)
13
+ content_type = content_type.to_sym
14
+ unless Helpers::Confidence::CONTENT_TYPES.include?(content_type)
15
+ raise ArgumentError, "invalid content_type: #{content_type}. Must be one of #{Helpers::Confidence::CONTENT_TYPES}"
16
+ end
17
+
18
+ {
19
+ action: :store,
20
+ content: content,
21
+ content_type: content_type,
22
+ tags: Array(tags),
23
+ source_agent: source_agent,
24
+ context: context
25
+ }
26
+ end
27
+
28
+ def query_knowledge(query:, limit: 10, min_confidence: 0.3, status: [:confirmed], tags: nil, **)
29
+ {
30
+ action: :query,
31
+ query: query,
32
+ limit: limit,
33
+ min_confidence: min_confidence,
34
+ status: Array(status),
35
+ tags: tags
36
+ }
37
+ end
38
+
39
+ def related_entries(entry_id:, relation_types: nil, depth: 2, **)
40
+ {
41
+ action: :traverse,
42
+ entry_id: entry_id,
43
+ relation_types: relation_types,
44
+ depth: depth
45
+ }
46
+ end
47
+
48
+ def deprecate_entry(entry_id:, reason:, **)
49
+ {
50
+ action: :deprecate,
51
+ entry_id: entry_id,
52
+ reason: reason
53
+ }
54
+ end
55
+
56
+ def handle_ingest(content:, content_type:, tags: [], source_agent: 'unknown', context: {}, **)
57
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
58
+
59
+ embedding = Helpers::Embedding.generate(text: content)
60
+ content_type_sym = content_type.to_s
61
+ tag_array = Array(tags)
62
+
63
+ corroborated, existing_id = find_corroboration(embedding, content_type_sym, source_agent)
64
+
65
+ unless corroborated
66
+ new_entry = Legion::Data::Model::ApolloEntry.create(
67
+ content: content,
68
+ content_type: content_type_sym,
69
+ confidence: Helpers::Confidence::INITIAL_CONFIDENCE,
70
+ source_agent: source_agent,
71
+ source_context: ::JSON.dump(context.is_a?(Hash) ? context : {}),
72
+ tags: Sequel.pg_array(tag_array),
73
+ status: 'candidate',
74
+ embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")
75
+ )
76
+ existing_id = new_entry.id
77
+ end
78
+
79
+ upsert_expertise(source_agent: source_agent, domain: tag_array.first || 'general')
80
+
81
+ Legion::Data::Model::ApolloAccessLog.create(
82
+ entry_id: existing_id, agent_id: source_agent, action: 'ingest'
83
+ )
84
+
85
+ { success: true, entry_id: existing_id, status: corroborated ? 'corroborated' : 'candidate',
86
+ corroborated: corroborated }
87
+ rescue Sequel::Error => e
88
+ { success: false, error: e.message }
89
+ end
90
+
91
+ def handle_query(query:, limit: 10, min_confidence: 0.3, status: [:confirmed], tags: nil, agent_id: 'unknown', **) # rubocop:disable Metrics/ParameterLists
92
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
93
+
94
+ embedding = Helpers::Embedding.generate(text: query)
95
+ sql = Helpers::GraphQuery.build_semantic_search_sql(
96
+ limit: limit, min_confidence: min_confidence,
97
+ statuses: Array(status).map(&:to_s), tags: tags
98
+ )
99
+
100
+ db = Legion::Data::Model::ApolloEntry.db
101
+ entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
102
+
103
+ entries.each do |entry|
104
+ Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
105
+ access_count: Sequel.expr(:access_count) + 1,
106
+ confidence: Helpers::Confidence.apply_retrieval_boost(
107
+ confidence: entry[:confidence]
108
+ ),
109
+ updated_at: Time.now
110
+ )
111
+ end
112
+
113
+ if entries.any?
114
+ Legion::Data::Model::ApolloAccessLog.create(
115
+ entry_id: entries.first&.dig(:id), agent_id: agent_id, action: 'query'
116
+ )
117
+ end
118
+
119
+ formatted = entries.map do |entry|
120
+ { id: entry[:id], content: entry[:content], content_type: entry[:content_type],
121
+ confidence: entry[:confidence], distance: entry[:distance],
122
+ tags: entry[:tags], source_agent: entry[:source_agent] }
123
+ end
124
+
125
+ { success: true, entries: formatted, count: formatted.size }
126
+ rescue Sequel::Error => e
127
+ { success: false, error: e.message }
128
+ end
129
+
130
+ def retrieve_relevant(query: nil, limit: 5, min_confidence: 0.3, tags: nil, skip: false, **)
131
+ return { status: :skipped } if skip
132
+
133
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
134
+
135
+ return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
136
+
137
+ embedding = Helpers::Embedding.generate(text: query.to_s)
138
+ sql = Helpers::GraphQuery.build_semantic_search_sql(
139
+ limit: limit, min_confidence: min_confidence,
140
+ statuses: ['confirmed'], tags: tags
141
+ )
142
+
143
+ db = Legion::Data::Model::ApolloEntry.db
144
+ entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
145
+
146
+ entries.each do |entry|
147
+ Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
148
+ confidence: Helpers::Confidence.apply_retrieval_boost(confidence: entry[:confidence]),
149
+ updated_at: Time.now
150
+ )
151
+ end
152
+
153
+ formatted = entries.map do |entry|
154
+ { id: entry[:id], content: entry[:content], content_type: entry[:content_type],
155
+ confidence: entry[:confidence], distance: entry[:distance],
156
+ tags: entry[:tags], source_agent: entry[:source_agent] }
157
+ end
158
+
159
+ { success: true, entries: formatted, count: formatted.size }
160
+ rescue Sequel::Error => e
161
+ { success: false, error: e.message }
162
+ end
163
+
164
+ private
165
+
166
+ def find_corroboration(embedding, content_type_sym, source_agent)
167
+ existing = Legion::Data::Model::ApolloEntry
168
+ .where(content_type: content_type_sym)
169
+ .exclude(embedding: nil)
170
+ .limit(50)
171
+
172
+ existing.each do |entry|
173
+ next unless entry.embedding
174
+
175
+ sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: entry.embedding)
176
+ next unless Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
177
+
178
+ entry.update(
179
+ confidence: Helpers::Confidence.apply_corroboration_boost(confidence: entry.confidence),
180
+ updated_at: Time.now
181
+ )
182
+ Legion::Data::Model::ApolloRelation.create(
183
+ from_entry_id: entry.id,
184
+ to_entry_id: entry.id,
185
+ relation_type: 'similar_to',
186
+ source_agent: source_agent,
187
+ weight: sim
188
+ )
189
+ return [true, entry.id]
190
+ end
191
+
192
+ [false, nil]
193
+ end
194
+
195
+ def upsert_expertise(source_agent:, domain:)
196
+ expertise = Legion::Data::Model::ApolloExpertise
197
+ .where(agent_id: source_agent, domain: domain).first
198
+ if expertise
199
+ expertise.update(entry_count: expertise.entry_count + 1, last_active_at: Time.now)
200
+ else
201
+ Legion::Data::Model::ApolloExpertise.create(
202
+ agent_id: source_agent, domain: domain, proficiency: 0.0,
203
+ entry_count: 1, last_active_at: Time.now
204
+ )
205
+ end
206
+ end
207
+
208
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/confidence'
4
+ require_relative '../helpers/similarity'
5
+ require_relative '../helpers/embedding'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Apollo
10
+ module Runners
11
+ module Maintenance
12
+ def force_decay(factor: 0.5, **)
13
+ { action: :force_decay, factor: factor }
14
+ end
15
+
16
+ def archive_stale(days: 90, **)
17
+ { action: :archive_stale, days: days }
18
+ end
19
+
20
+ def resolve_dispute(entry_id:, resolution:, **)
21
+ { action: :resolve_dispute, entry_id: entry_id, resolution: resolution }
22
+ end
23
+
24
+ def check_corroboration(**)
25
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
26
+
27
+ candidates = Legion::Data::Model::ApolloEntry.where(status: 'candidate').exclude(embedding: nil).all
28
+ confirmed = Legion::Data::Model::ApolloEntry.where(status: 'confirmed').exclude(embedding: nil).all
29
+
30
+ promoted = 0
31
+
32
+ candidates.each do |candidate|
33
+ match = confirmed.find do |conf|
34
+ next unless conf.content_type == candidate.content_type
35
+
36
+ sim = Helpers::Similarity.cosine_similarity(
37
+ vec_a: candidate.embedding, vec_b: conf.embedding
38
+ )
39
+ Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
40
+ end
41
+
42
+ next unless match
43
+
44
+ candidate.update(
45
+ status: 'confirmed',
46
+ confirmed_at: Time.now,
47
+ confidence: Helpers::Confidence.apply_corroboration_boost(confidence: candidate.confidence),
48
+ updated_at: Time.now
49
+ )
50
+
51
+ Legion::Data::Model::ApolloRelation.create(
52
+ from_entry_id: candidate.id,
53
+ to_entry_id: match.id,
54
+ relation_type: 'similar_to',
55
+ source_agent: 'system:corroboration',
56
+ weight: 1.0
57
+ )
58
+
59
+ promoted += 1
60
+ end
61
+
62
+ { success: true, promoted: promoted, scanned: candidates.size }
63
+ rescue Sequel::Error => e
64
+ { success: false, error: e.message }
65
+ end
66
+
67
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport/exchange' if defined?(Legion::Transport)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Transport
9
+ module Exchanges
10
+ class Apollo < Legion::Transport::Exchange
11
+ def exchange_name
12
+ 'apollo'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport/message' if defined?(Legion::Transport)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Transport
9
+ module Messages
10
+ class Ingest < Legion::Transport::Message
11
+ def exchange
12
+ Exchanges::Apollo
13
+ end
14
+
15
+ def routing_key
16
+ 'apollo.ingest'
17
+ end
18
+
19
+ def message
20
+ {
21
+ content: @options[:content],
22
+ content_type: @options[:content_type],
23
+ tags: @options[:tags],
24
+ source_agent: @options[:source_agent],
25
+ context: @options[:context] || {}
26
+ }
27
+ end
28
+
29
+ def type
30
+ 'apollo_ingest'
31
+ end
32
+
33
+ def validate
34
+ raise TypeError, 'content is required' unless @options[:content].is_a?(String)
35
+
36
+ @valid = true
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport/message' if defined?(Legion::Transport)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Transport
9
+ module Messages
10
+ class Query < Legion::Transport::Message
11
+ def exchange
12
+ Exchanges::Apollo
13
+ end
14
+
15
+ def routing_key
16
+ 'apollo.query'
17
+ end
18
+
19
+ def message
20
+ {
21
+ action: @options[:action],
22
+ query: @options[:query],
23
+ entry_id: @options[:entry_id],
24
+ limit: @options[:limit],
25
+ min_confidence: @options[:min_confidence],
26
+ status: @options[:status],
27
+ tags: @options[:tags],
28
+ relation_types: @options[:relation_types],
29
+ depth: @options[:depth],
30
+ reply_to: @options[:reply_to],
31
+ correlation_id: @options[:correlation_id]
32
+ }.compact
33
+ end
34
+
35
+ def type
36
+ 'apollo_query'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport/queue' if defined?(Legion::Transport)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Transport
9
+ module Queues
10
+ class Ingest < Legion::Transport::Queue
11
+ def queue_name
12
+ 'apollo.ingest'
13
+ end
14
+
15
+ def queue_options
16
+ { manual_ack: true, durable: true, arguments: { 'x-dead-letter-exchange': 'apollo.dlx' } }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport/queue' if defined?(Legion::Transport)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Transport
9
+ module Queues
10
+ class Query < Legion::Transport::Queue
11
+ def queue_name
12
+ 'apollo.query'
13
+ end
14
+
15
+ def queue_options
16
+ { manual_ack: true, durable: true, arguments: { 'x-dead-letter-exchange': 'apollo.dlx' } }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ VERSION = '0.2.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/apollo/version'
4
+ require 'legion/extensions/apollo/helpers/confidence'
5
+ require 'legion/extensions/apollo/helpers/similarity'
6
+ require 'legion/extensions/apollo/helpers/graph_query'
7
+ require 'legion/extensions/apollo/runners/knowledge'
8
+ require 'legion/extensions/apollo/runners/expertise'
9
+ require 'legion/extensions/apollo/runners/maintenance'
10
+
11
+ if defined?(Legion::Transport)
12
+ require 'legion/extensions/apollo/transport/exchanges/apollo'
13
+ require 'legion/extensions/apollo/transport/queues/ingest'
14
+ require 'legion/extensions/apollo/transport/queues/query'
15
+ require 'legion/extensions/apollo/transport/messages/ingest'
16
+ require 'legion/extensions/apollo/transport/messages/query'
17
+ end
18
+
19
+ module Legion
20
+ module Extensions
21
+ module Apollo
22
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ unless defined?(Legion::Extensions::Actors::Every)
6
+ module Legion
7
+ module Extensions
8
+ module Actors
9
+ class Every; end # rubocop:disable Lint/EmptyClass
10
+ end
11
+ end
12
+ end
13
+ end
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every' unless $LOADED_FEATURES.include?('legion/extensions/actors/every')
15
+
16
+ require 'legion/extensions/apollo/runners/maintenance'
17
+ require 'legion/extensions/apollo/actors/decay'
18
+
19
+ RSpec.describe Legion::Extensions::Apollo::Actor::Decay do
20
+ subject(:actor) { described_class.new }
21
+
22
+ it 'uses Maintenance runner_class' do
23
+ expect(actor.runner_class).to eq(Legion::Extensions::Apollo::Runners::Maintenance)
24
+ end
25
+
26
+ it 'runs force_decay function' do
27
+ expect(actor.runner_function).to eq('force_decay')
28
+ end
29
+
30
+ it 'runs every 3600 seconds' do
31
+ expect(actor.time).to eq(3600)
32
+ end
33
+
34
+ it 'does not run immediately' do
35
+ expect(actor.run_now?).to be false
36
+ end
37
+
38
+ it 'does not use runner framework' do
39
+ expect(actor.use_runner?).to be false
40
+ end
41
+
42
+ it 'does not generate tasks' do
43
+ expect(actor.generate_task?).to be false
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ unless defined?(Legion::Extensions::Actors::Every)
6
+ module Legion
7
+ module Extensions
8
+ module Actors
9
+ class Every; end # rubocop:disable Lint/EmptyClass
10
+ end
11
+ end
12
+ end
13
+ end
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every' unless $LOADED_FEATURES.include?('legion/extensions/actors/every')
15
+
16
+ require 'legion/extensions/apollo/runners/expertise'
17
+ require 'legion/extensions/apollo/actors/expertise_aggregator'
18
+
19
+ RSpec.describe Legion::Extensions::Apollo::Actor::ExpertiseAggregator do
20
+ subject(:actor) { described_class.new }
21
+
22
+ it 'uses Expertise runner_class' do
23
+ expect(actor.runner_class).to eq(Legion::Extensions::Apollo::Runners::Expertise)
24
+ end
25
+
26
+ it 'runs aggregate function' do
27
+ expect(actor.runner_function).to eq('aggregate')
28
+ end
29
+
30
+ it 'runs every 1800 seconds' do
31
+ expect(actor.time).to eq(1800)
32
+ end
33
+
34
+ it 'does not run immediately' do
35
+ expect(actor.run_now?).to be false
36
+ end
37
+
38
+ it 'does not generate tasks' do
39
+ expect(actor.generate_task?).to be false
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ unless defined?(Legion::Extensions::Actors::Subscription)
6
+ module Legion
7
+ module Extensions
8
+ module Actors
9
+ class Subscription; end # rubocop:disable Lint/EmptyClass
10
+ end
11
+ end
12
+ end
13
+ end
14
+ $LOADED_FEATURES << 'legion/extensions/actors/subscription' unless $LOADED_FEATURES.include?('legion/extensions/actors/subscription')
15
+
16
+ require 'legion/extensions/apollo/runners/knowledge'
17
+ require 'legion/extensions/apollo/actors/ingest'
18
+
19
+ RSpec.describe Legion::Extensions::Apollo::Actor::Ingest do
20
+ subject(:actor) { described_class.new }
21
+
22
+ it 'uses Knowledge runner_class as string' do
23
+ expect(actor.runner_class).to eq('Legion::Extensions::Apollo::Runners::Knowledge')
24
+ end
25
+
26
+ it 'runs handle_ingest function' do
27
+ expect(actor.runner_function).to eq('handle_ingest')
28
+ end
29
+
30
+ it 'does not generate tasks' do
31
+ expect(actor.generate_task?).to be false
32
+ end
33
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/apollo/client'
5
+
6
+ RSpec.describe Legion::Extensions::Apollo::Client do
7
+ let(:client) { described_class.new(agent_id: 'test-agent') }
8
+
9
+ describe '#initialize' do
10
+ it 'stores agent_id' do
11
+ expect(client.agent_id).to eq('test-agent')
12
+ end
13
+
14
+ it 'defaults agent_id to unknown' do
15
+ c = described_class.new
16
+ expect(c.agent_id).to eq('unknown')
17
+ end
18
+ end
19
+
20
+ describe 'Knowledge runner' do
21
+ it 'responds to store_knowledge' do
22
+ expect(client).to respond_to(:store_knowledge)
23
+ end
24
+
25
+ it 'injects source_agent from client agent_id' do
26
+ result = client.store_knowledge(content: 'test fact', content_type: :fact)
27
+ expect(result[:source_agent]).to eq('test-agent')
28
+ end
29
+
30
+ it 'allows source_agent override' do
31
+ result = client.store_knowledge(content: 'test', content_type: :fact, source_agent: 'other')
32
+ expect(result[:source_agent]).to eq('other')
33
+ end
34
+
35
+ it 'responds to query_knowledge' do
36
+ expect(client).to respond_to(:query_knowledge)
37
+ end
38
+
39
+ it 'responds to related_entries' do
40
+ expect(client).to respond_to(:related_entries)
41
+ end
42
+
43
+ it 'responds to deprecate_entry' do
44
+ expect(client).to respond_to(:deprecate_entry)
45
+ end
46
+ end
47
+
48
+ describe 'Expertise runner' do
49
+ it 'responds to get_expertise' do
50
+ expect(client).to respond_to(:get_expertise)
51
+ end
52
+
53
+ it 'responds to domains_at_risk' do
54
+ expect(client).to respond_to(:domains_at_risk)
55
+ end
56
+
57
+ it 'responds to agent_profile' do
58
+ expect(client).to respond_to(:agent_profile)
59
+ end
60
+ end
61
+
62
+ describe 'Maintenance runner' do
63
+ it 'responds to force_decay' do
64
+ expect(client).to respond_to(:force_decay)
65
+ end
66
+
67
+ it 'responds to archive_stale' do
68
+ expect(client).to respond_to(:archive_stale)
69
+ end
70
+
71
+ it 'responds to resolve_dispute' do
72
+ expect(client).to respond_to(:resolve_dispute)
73
+ end
74
+ end
75
+ end