lex-schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 610c292616da34f0d31990db52cd23f1f6144003742f28c5fa10482fbbe8f490
4
+ data.tar.gz: d0d06626d42e4b37bf60ffb78284c4a8cbd04fb5f59bfe1ea2e3835de2b203d9
5
+ SHA512:
6
+ metadata.gz: c4f0d6677c40e2ef05393c4c122bf5072d20115a442275ebd145f857165cf43099383697c942205662bed81afa110e4bbbf02f8ad6441fb3cf0e6b51d623f1e9
7
+ data.tar.gz: ce3fa23aa8352a05e629c9bd045ca24e859e40c62c8a952f522dae0439b985575ebd85980e39823a77acd2643c76342bcf521b7327c4044999a42b4c694e9f1f
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/schema/helpers/constants'
4
+ require 'legion/extensions/schema/helpers/causal_relation'
5
+ require 'legion/extensions/schema/helpers/world_model'
6
+ require 'legion/extensions/schema/runners/schema'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Schema
11
+ class Client
12
+ include Runners::Schema
13
+
14
+ attr_reader :world_model
15
+
16
+ def initialize(world_model: nil, **)
17
+ @world_model = world_model || Helpers::WorldModel.new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Schema
8
+ module Helpers
9
+ class CausalRelation
10
+ attr_reader :id, :cause, :effect, :relation_type, :confidence,
11
+ :evidence_count, :created_at, :updated_at
12
+
13
+ def initialize(cause:, effect:, relation_type:, confidence: 0.5)
14
+ @id = SecureRandom.uuid
15
+ @cause = cause
16
+ @effect = effect
17
+ @relation_type = relation_type
18
+ @confidence = confidence.clamp(0.0, 1.0)
19
+ @evidence_count = 1
20
+ @created_at = Time.now.utc
21
+ @updated_at = Time.now.utc
22
+ end
23
+
24
+ def reinforce(amount = Constants::REINFORCEMENT_BONUS)
25
+ @confidence = [@confidence + amount, 1.0].min
26
+ @evidence_count += 1
27
+ @updated_at = Time.now.utc
28
+ end
29
+
30
+ def weaken(amount = Constants::CONTRADICTION_PENALTY)
31
+ @confidence = [@confidence - amount, 0.0].max
32
+ @updated_at = Time.now.utc
33
+ end
34
+
35
+ def decay
36
+ @confidence = [@confidence - Constants::DECAY_RATE, 0.0].max
37
+ end
38
+
39
+ def established?
40
+ @confidence >= Constants::CONFIDENCE_LEVELS[:established]
41
+ end
42
+
43
+ def speculative?
44
+ @confidence <= Constants::CONFIDENCE_LEVELS[:speculative]
45
+ end
46
+
47
+ def prunable?
48
+ @confidence < Constants::PRUNE_THRESHOLD
49
+ end
50
+
51
+ def to_h
52
+ {
53
+ id: @id,
54
+ cause: @cause,
55
+ effect: @effect,
56
+ relation_type: @relation_type,
57
+ confidence: @confidence.round(4),
58
+ evidence_count: @evidence_count,
59
+ established: established?
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Schema
6
+ module Helpers
7
+ module Constants
8
+ RELATION_TYPES = %i[
9
+ causes
10
+ prevents
11
+ enables
12
+ requires
13
+ correlates
14
+ contradicts
15
+ ].freeze
16
+
17
+ CONFIDENCE_LEVELS = {
18
+ established: 0.9,
19
+ strong: 0.7,
20
+ moderate: 0.5,
21
+ weak: 0.3,
22
+ speculative: 0.1
23
+ }.freeze
24
+
25
+ SCHEMA_ALPHA = 0.12
26
+
27
+ MAX_SCHEMAS = 500
28
+
29
+ MAX_RELATIONS_PER_SCHEMA = 50
30
+
31
+ REINFORCEMENT_BONUS = 0.05
32
+
33
+ CONTRADICTION_PENALTY = 0.15
34
+
35
+ DECAY_RATE = 0.005
36
+
37
+ PRUNE_THRESHOLD = 0.1
38
+
39
+ MAX_COUNTERFACTUAL_DEPTH = 5
40
+
41
+ MAX_EXPLANATION_CHAIN = 10
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Schema
6
+ module Helpers
7
+ class WorldModel
8
+ attr_reader :relations, :domains
9
+
10
+ def initialize
11
+ @relations = {}
12
+ @domains = Hash.new { |h, k| h[k] = [] }
13
+ end
14
+
15
+ def add_relation(cause:, effect:, relation_type:, confidence: 0.5)
16
+ return nil unless Constants::RELATION_TYPES.include?(relation_type)
17
+
18
+ key = relation_key(cause, effect, relation_type)
19
+ if @relations.key?(key)
20
+ @relations[key].reinforce
21
+ else
22
+ @relations[key] = CausalRelation.new(cause: cause, effect: effect, relation_type: relation_type, confidence: confidence)
23
+ index_domains(cause, effect, key)
24
+ trim_relations
25
+ end
26
+ @relations[key]
27
+ end
28
+
29
+ def weaken_relation(cause:, effect:, relation_type:)
30
+ key = relation_key(cause, effect, relation_type)
31
+ relation = @relations[key]
32
+ return nil unless relation
33
+
34
+ relation.weaken
35
+ prune_if_needed(key)
36
+ relation
37
+ end
38
+
39
+ def find_effects(cause)
40
+ keys = @domains[cause] || []
41
+ keys.filter_map { |k| @relations[k] }.select { |r| r.cause == cause }
42
+ end
43
+
44
+ def find_causes(effect)
45
+ keys = @domains[effect] || []
46
+ keys.filter_map { |k| @relations[k] }.select { |r| r.effect == effect }
47
+ end
48
+
49
+ def explain(outcome, max_depth: Constants::MAX_EXPLANATION_CHAIN)
50
+ chain = []
51
+ visited = Set.new
52
+ build_explanation_chain(outcome, chain, visited, max_depth)
53
+ chain
54
+ end
55
+
56
+ def counterfactual(cause, max_depth: Constants::MAX_COUNTERFACTUAL_DEPTH)
57
+ affected = []
58
+ visited = Set.new
59
+ propagate_counterfactual(cause, affected, visited, max_depth)
60
+ affected
61
+ end
62
+
63
+ def contradictions
64
+ @relations.values.each_with_object([]) do |rel, result|
65
+ next unless rel.relation_type == :causes
66
+
67
+ opposite = @relations[relation_key(rel.cause, rel.effect, :prevents)]
68
+ next unless opposite
69
+
70
+ result << {
71
+ cause: rel.cause,
72
+ effect: rel.effect,
73
+ causes_confidence: rel.confidence.round(4),
74
+ prevents_confidence: opposite.confidence.round(4)
75
+ }
76
+ end
77
+ end
78
+
79
+ def decay_all
80
+ @relations.each_value(&:decay)
81
+ prune_weak
82
+ end
83
+
84
+ def relation_count
85
+ @relations.size
86
+ end
87
+
88
+ def domain_count
89
+ all_entities = @relations.values.flat_map { |r| [r.cause, r.effect] }
90
+ all_entities.uniq.size
91
+ end
92
+
93
+ def established_relations
94
+ @relations.values.select(&:established?)
95
+ end
96
+
97
+ def to_h
98
+ {
99
+ relation_count: relation_count,
100
+ domain_count: domain_count,
101
+ established_count: established_relations.size,
102
+ contradiction_count: contradictions.size
103
+ }
104
+ end
105
+
106
+ private
107
+
108
+ def relation_key(cause, effect, relation_type)
109
+ "#{cause}->#{relation_type}->#{effect}"
110
+ end
111
+
112
+ def index_domains(cause, effect, key)
113
+ @domains[cause] << key
114
+ @domains[effect] << key
115
+ end
116
+
117
+ def build_explanation_chain(outcome, chain, visited, depth)
118
+ return if depth <= 0 || visited.include?(outcome)
119
+
120
+ visited.add(outcome)
121
+ causes = find_causes(outcome).sort_by { |r| -r.confidence }
122
+ causes.first(3).each do |rel|
123
+ chain << rel.to_h
124
+ build_explanation_chain(rel.cause, chain, visited, depth - 1)
125
+ end
126
+ end
127
+
128
+ def propagate_counterfactual(cause, affected, visited, depth)
129
+ return if depth <= 0 || visited.include?(cause)
130
+
131
+ visited.add(cause)
132
+ effects = find_effects(cause).sort_by { |r| -r.confidence }
133
+ effects.each do |rel|
134
+ affected << { effect: rel.effect, relation: rel.relation_type, confidence: rel.confidence.round(4) }
135
+ propagate_counterfactual(rel.effect, affected, visited, depth - 1)
136
+ end
137
+ end
138
+
139
+ def prune_if_needed(key)
140
+ relation = @relations[key]
141
+ return unless relation&.prunable?
142
+
143
+ remove_relation(key)
144
+ end
145
+
146
+ def prune_weak
147
+ prunable = @relations.select { |_, r| r.prunable? }.keys
148
+ prunable.each { |key| remove_relation(key) }
149
+ end
150
+
151
+ def remove_relation(key)
152
+ rel = @relations.delete(key)
153
+ return unless rel
154
+
155
+ @domains[rel.cause]&.delete(key)
156
+ @domains[rel.effect]&.delete(key)
157
+ end
158
+
159
+ def trim_relations
160
+ return if @relations.size <= Constants::MAX_SCHEMAS
161
+
162
+ weakest = @relations.sort_by { |_, r| r.confidence }
163
+ weakest.first(@relations.size - Constants::MAX_SCHEMAS).each { |k, _| remove_relation(k) } # rubocop:disable Style/HashEachMethods
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Schema
6
+ module Runners
7
+ module Schema
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def update_schema(tick_results: {}, **)
12
+ extract_prediction_outcomes(tick_results)
13
+ world_model.decay_all
14
+
15
+ Legion::Logging.debug "[schema] relations=#{world_model.relation_count} " \
16
+ "domains=#{world_model.domain_count} established=#{world_model.established_relations.size}"
17
+
18
+ world_model.to_h
19
+ end
20
+
21
+ def learn_relation(cause:, effect:, relation_type:, confidence: 0.5, **)
22
+ relation_sym = relation_type.to_sym
23
+ result = world_model.add_relation(cause: cause, effect: effect, relation_type: relation_sym, confidence: confidence)
24
+ return { success: false, error: 'invalid relation type' } unless result
25
+
26
+ Legion::Logging.info "[schema] learned: #{cause} #{relation_sym} #{effect} (#{result.confidence.round(2)})"
27
+ { success: true, relation: result.to_h }
28
+ end
29
+
30
+ def weaken_relation(cause:, effect:, relation_type:, **)
31
+ result = world_model.weaken_relation(cause: cause, effect: effect, relation_type: relation_type.to_sym)
32
+ return { success: false, error: 'relation not found' } unless result
33
+
34
+ Legion::Logging.debug "[schema] weakened: #{cause} #{relation_type} #{effect}"
35
+ { success: true, relation: result.to_h }
36
+ end
37
+
38
+ def explain(outcome:, **)
39
+ chain = world_model.explain(outcome)
40
+ Legion::Logging.debug "[schema] explanation for #{outcome}: #{chain.size} links"
41
+ { outcome: outcome, chain: chain, depth: chain.size }
42
+ end
43
+
44
+ def counterfactual(cause:, **)
45
+ affected = world_model.counterfactual(cause)
46
+ Legion::Logging.debug "[schema] counterfactual for #{cause}: #{affected.size} effects"
47
+ { cause: cause, affected: affected, impact: affected.size }
48
+ end
49
+
50
+ def find_effects(cause:, **)
51
+ effects = world_model.find_effects(cause).map(&:to_h)
52
+ { cause: cause, effects: effects, count: effects.size }
53
+ end
54
+
55
+ def find_causes(effect:, **)
56
+ causes = world_model.find_causes(effect).map(&:to_h)
57
+ { effect: effect, causes: causes, count: causes.size }
58
+ end
59
+
60
+ def contradictions(**)
61
+ result = world_model.contradictions
62
+ Legion::Logging.debug "[schema] contradictions: #{result.size}"
63
+ { contradictions: result, count: result.size }
64
+ end
65
+
66
+ def schema_stats(**)
67
+ Legion::Logging.debug '[schema] stats'
68
+ world_model.to_h.merge(
69
+ top_relations: world_model.established_relations.first(10).map(&:to_h)
70
+ )
71
+ end
72
+
73
+ private
74
+
75
+ def world_model
76
+ @world_model ||= Helpers::WorldModel.new
77
+ end
78
+
79
+ def extract_prediction_outcomes(tick_results)
80
+ predictions = tick_results.dig(:prediction_engine, :resolved)
81
+ return unless predictions.is_a?(Array)
82
+
83
+ predictions.each do |pred|
84
+ next unless pred[:domain] && pred[:outcome_domain]
85
+
86
+ if pred[:accurate]
87
+ world_model.add_relation(cause: pred[:domain], effect: pred[:outcome_domain], relation_type: :causes, confidence: 0.6)
88
+ else
89
+ world_model.weaken_relation(cause: pred[:domain], effect: pred[:outcome_domain], relation_type: :causes)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Schema
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/schema/version'
4
+ require 'legion/extensions/schema/helpers/constants'
5
+ require 'legion/extensions/schema/helpers/causal_relation'
6
+ require 'legion/extensions/schema/helpers/world_model'
7
+ require 'legion/extensions/schema/runners/schema'
8
+ require 'legion/extensions/schema/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Schema
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-schema
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Builds and maintains causal schemas — structured models of how things
27
+ work, enabling explanation and counterfactual reasoning
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/schema.rb
35
+ - lib/legion/extensions/schema/client.rb
36
+ - lib/legion/extensions/schema/helpers/causal_relation.rb
37
+ - lib/legion/extensions/schema/helpers/constants.rb
38
+ - lib/legion/extensions/schema/helpers/world_model.rb
39
+ - lib/legion/extensions/schema/runners/schema.rb
40
+ - lib/legion/extensions/schema/version.rb
41
+ homepage: https://github.com/LegionIO/lex-schema
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: Internal world model and causal reasoning for LegionIO cognitive agents
63
+ test_files: []