lex-dual-process 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 +7 -0
- data/Gemfile +15 -0
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/lex-dual-process.gemspec +29 -0
- data/lib/legion/extensions/dual_process/client.rb +21 -0
- data/lib/legion/extensions/dual_process/helpers/constants.rb +40 -0
- data/lib/legion/extensions/dual_process/helpers/decision.rb +46 -0
- data/lib/legion/extensions/dual_process/helpers/dual_process_engine.rb +196 -0
- data/lib/legion/extensions/dual_process/helpers/heuristic.rb +63 -0
- data/lib/legion/extensions/dual_process/runners/dual_process.rb +83 -0
- data/lib/legion/extensions/dual_process/version.rb +9 -0
- data/lib/legion/extensions/dual_process.rb +16 -0
- data/spec/legion/extensions/dual_process/client_spec.rb +74 -0
- data/spec/legion/extensions/dual_process/helpers/constants_spec.rb +55 -0
- data/spec/legion/extensions/dual_process/helpers/decision_spec.rb +76 -0
- data/spec/legion/extensions/dual_process/helpers/dual_process_engine_spec.rb +274 -0
- data/spec/legion/extensions/dual_process/helpers/heuristic_spec.rb +144 -0
- data/spec/legion/extensions/dual_process/runners/dual_process_spec.rb +188 -0
- data/spec/spec_helper.rb +24 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d2f51b67089c8d64b94d67427a3f892d7a65924e24e12203092c3fdcddda73cd
|
|
4
|
+
data.tar.gz: e742fa6726c11c5fad560a709daad2ec95fb3c0435d49450df99c69f9828e8f8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 54df463d20f44b00ed14c43901db6cd50012049b4ae934fadfc299e231156264c21ca10916c8b184ec064ab7a3f2ef96e6a6f3ab4db82f1640b2a75db3e7c9e7
|
|
7
|
+
data.tar.gz: 960ade33ca509551591ea7dfe577b4d4afe1a35374ae18fbccaf1a98952f6e75d16d49b4c729532c742d1bbdd40967b1b1504eef9e04ede9fedb41a49b96374d
|
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec', '~> 3.13'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop', '~> 1.75', require: false
|
|
11
|
+
gem 'rubocop-rspec', require: false
|
|
12
|
+
gem 'simplecov'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Iverson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# lex-dual-process
|
|
2
|
+
|
|
3
|
+
Dual-process cognition modeling for the LegionIO brain-modeled cognitive architecture.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Implements Kahneman's System 1 / System 2 theory. Routes decisions to fast heuristic reasoning (System 1) or slow deliberative reasoning (System 2) based on query complexity, confidence threshold, and available effort budget. Maintains a library of learned heuristics that improve through reinforcement. Models cognitive fatigue via a depletable effort budget that recovers over time.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client = Legion::Extensions::DualProcess::Client.new
|
|
13
|
+
|
|
14
|
+
# Register a heuristic pattern
|
|
15
|
+
client.register_heuristic(
|
|
16
|
+
pattern: 'familiar HTTP request',
|
|
17
|
+
domain: :networking,
|
|
18
|
+
confidence: 0.8
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Route a decision — automatically selects System 1 or System 2
|
|
22
|
+
client.route_decision(query: 'handle incoming request', domain: :networking, complexity: 0.3)
|
|
23
|
+
# => { success: true, system: :system_one, confidence: 0.8, complexity: 0.3,
|
|
24
|
+
# heuristic_used: "...", effort_remaining: 1.0 }
|
|
25
|
+
|
|
26
|
+
# High-complexity query forces System 2
|
|
27
|
+
client.route_decision(query: 'design new architecture', domain: :planning, complexity: 0.9)
|
|
28
|
+
# => { success: true, system: :system_two, confidence: 0.7, effort_cost: 0.1, ... }
|
|
29
|
+
|
|
30
|
+
# Record outcome to reinforce the heuristic
|
|
31
|
+
client.record_decision_outcome(decision_id: '...', success: true)
|
|
32
|
+
|
|
33
|
+
# Check effort budget
|
|
34
|
+
client.effort_assessment
|
|
35
|
+
# => { effort_budget: 0.8, effort_label: :fluent, system_bias: :system_one }
|
|
36
|
+
|
|
37
|
+
# Best heuristics in a domain
|
|
38
|
+
client.best_heuristics(domain: :networking, limit: 5)
|
|
39
|
+
|
|
40
|
+
# Periodic tick: recover effort budget
|
|
41
|
+
client.update_dual_process
|
|
42
|
+
# => { success: true, effort_recovered: 0.05, effort_budget: 0.85 }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Routing Labels
|
|
46
|
+
|
|
47
|
+
| Effort Budget | Label |
|
|
48
|
+
|---|---|
|
|
49
|
+
| 0.8 – 1.0 | `:automatic` |
|
|
50
|
+
| 0.6 – 0.8 | `:fluent` |
|
|
51
|
+
| 0.4 – 0.6 | `:effortful` |
|
|
52
|
+
| 0.2 – 0.4 | `:strained` |
|
|
53
|
+
| 0.0 – 0.2 | `:depleted` |
|
|
54
|
+
|
|
55
|
+
When effort is depleted, all decisions route to System 1 regardless of complexity.
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bundle install
|
|
61
|
+
bundle exec rspec
|
|
62
|
+
bundle exec rubocop
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/dual_process/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-dual-process'
|
|
7
|
+
spec.version = Legion::Extensions::DualProcess::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Dual Process'
|
|
12
|
+
spec.description = "Kahneman's Dual Process Theory for brain-modeled agentic AI: System 1 (fast/intuitive) vs System 2 (slow/deliberate)"
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-dual-process'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-dual-process'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-dual-process'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-dual-process'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-dual-process/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-dual-process.gemspec Gemfile LICENSE README.md]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/dual_process/helpers/constants'
|
|
4
|
+
require 'legion/extensions/dual_process/helpers/heuristic'
|
|
5
|
+
require 'legion/extensions/dual_process/helpers/decision'
|
|
6
|
+
require 'legion/extensions/dual_process/helpers/dual_process_engine'
|
|
7
|
+
require 'legion/extensions/dual_process/runners/dual_process'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module DualProcess
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::DualProcess
|
|
14
|
+
|
|
15
|
+
def initialize(engine: nil)
|
|
16
|
+
@engine = engine || Helpers::DualProcessEngine.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DualProcess
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_DECISIONS = 200
|
|
9
|
+
MAX_HEURISTICS = 100
|
|
10
|
+
MAX_HISTORY = 300
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIDENCE = 0.5
|
|
13
|
+
CONFIDENCE_FLOOR = 0.05
|
|
14
|
+
CONFIDENCE_CEILING = 0.95
|
|
15
|
+
SYSTEM_ONE_THRESHOLD = 0.6
|
|
16
|
+
COMPLEXITY_THRESHOLD = 0.5
|
|
17
|
+
|
|
18
|
+
EFFORT_COST = 0.1
|
|
19
|
+
EFFORT_RECOVERY_RATE = 0.05
|
|
20
|
+
MAX_EFFORT_BUDGET = 1.0
|
|
21
|
+
FATIGUE_PENALTY = 0.15
|
|
22
|
+
HEURISTIC_BOOST = 0.2
|
|
23
|
+
DECAY_RATE = 0.01
|
|
24
|
+
|
|
25
|
+
SYSTEMS = %i[system_one system_two].freeze
|
|
26
|
+
|
|
27
|
+
ROUTING_LABELS = {
|
|
28
|
+
(0.8..) => :automatic,
|
|
29
|
+
(0.6...0.8) => :fluent,
|
|
30
|
+
(0.4...0.6) => :effortful,
|
|
31
|
+
(0.2...0.4) => :strained,
|
|
32
|
+
(..0.2) => :depleted
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
DECISION_OUTCOMES = %i[correct incorrect uncertain].freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module DualProcess
|
|
8
|
+
module Helpers
|
|
9
|
+
class Decision
|
|
10
|
+
attr_accessor :outcome
|
|
11
|
+
attr_reader :id, :query, :domain, :system_used, :confidence, :complexity, :heuristic_id, :effort_cost, :processing_time_ms, :created_at
|
|
12
|
+
|
|
13
|
+
def initialize(query:, domain:, system_used:, confidence:, complexity:, **opts)
|
|
14
|
+
@id = SecureRandom.uuid
|
|
15
|
+
@query = query
|
|
16
|
+
@domain = domain
|
|
17
|
+
@system_used = system_used
|
|
18
|
+
@confidence = confidence.clamp(Constants::CONFIDENCE_FLOOR, Constants::CONFIDENCE_CEILING)
|
|
19
|
+
@complexity = complexity.clamp(0.0, 1.0)
|
|
20
|
+
@heuristic_id = opts.fetch(:heuristic_id, nil)
|
|
21
|
+
@outcome = nil
|
|
22
|
+
@effort_cost = opts.fetch(:effort_cost, 0.0)
|
|
23
|
+
@processing_time_ms = opts.fetch(:processing_time_ms, 0)
|
|
24
|
+
@created_at = Time.now.utc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
id: @id,
|
|
30
|
+
query: @query,
|
|
31
|
+
domain: @domain,
|
|
32
|
+
system_used: @system_used,
|
|
33
|
+
confidence: @confidence,
|
|
34
|
+
complexity: @complexity,
|
|
35
|
+
heuristic_id: @heuristic_id,
|
|
36
|
+
outcome: @outcome,
|
|
37
|
+
effort_cost: @effort_cost,
|
|
38
|
+
processing_time_ms: @processing_time_ms,
|
|
39
|
+
created_at: @created_at
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DualProcess
|
|
6
|
+
module Helpers
|
|
7
|
+
class DualProcessEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@effort_budget = MAX_EFFORT_BUDGET
|
|
12
|
+
@heuristics = {}
|
|
13
|
+
@decisions = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register_heuristic(pattern:, domain:, response:, confidence: DEFAULT_CONFIDENCE)
|
|
17
|
+
heuristic = Heuristic.new(pattern: pattern, domain: domain, response: response, confidence: confidence)
|
|
18
|
+
if @heuristics.size >= MAX_HEURISTICS
|
|
19
|
+
oldest_key = @heuristics.min_by { |_, h| h.last_used_at || h.created_at }.first
|
|
20
|
+
@heuristics.delete(oldest_key)
|
|
21
|
+
end
|
|
22
|
+
@heuristics[heuristic.id] = heuristic
|
|
23
|
+
heuristic
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def route_decision(query:, domain:, complexity:)
|
|
27
|
+
matching = find_matching_heuristic(query, domain)
|
|
28
|
+
use_system_one = complexity < COMPLEXITY_THRESHOLD &&
|
|
29
|
+
matching &&
|
|
30
|
+
matching.confidence >= SYSTEM_ONE_THRESHOLD
|
|
31
|
+
|
|
32
|
+
if use_system_one
|
|
33
|
+
{ system: :system_one, reason: :heuristic_match, heuristic_id: matching.id }
|
|
34
|
+
elsif @effort_budget >= EFFORT_COST
|
|
35
|
+
{ system: :system_two, reason: complexity_reason(complexity, matching) }
|
|
36
|
+
else
|
|
37
|
+
{ system: :system_one, reason: :effort_depleted, fatigue: true }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def execute_system_one(query:, domain:)
|
|
42
|
+
matching = find_matching_heuristic(query, domain)
|
|
43
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
|
|
45
|
+
if matching
|
|
46
|
+
matching.use!(success: true)
|
|
47
|
+
confidence = matching.confidence
|
|
48
|
+
response = matching.response
|
|
49
|
+
else
|
|
50
|
+
confidence = (DEFAULT_CONFIDENCE - FATIGUE_PENALTY).clamp(CONFIDENCE_FLOOR, CONFIDENCE_CEILING)
|
|
51
|
+
response = :no_heuristic
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
55
|
+
|
|
56
|
+
decision = Decision.new(
|
|
57
|
+
query: query,
|
|
58
|
+
domain: domain,
|
|
59
|
+
system_used: :system_one,
|
|
60
|
+
confidence: confidence,
|
|
61
|
+
complexity: 0.0,
|
|
62
|
+
heuristic_id: matching&.id,
|
|
63
|
+
effort_cost: 0.0,
|
|
64
|
+
processing_time_ms: elapsed
|
|
65
|
+
)
|
|
66
|
+
store_decision(decision)
|
|
67
|
+
{ decision_id: decision.id, system: :system_one, response: response, confidence: confidence }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_system_two(query:, domain:, deliberation: {})
|
|
71
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
72
|
+
@effort_budget = (@effort_budget - EFFORT_COST).clamp(0.0, MAX_EFFORT_BUDGET)
|
|
73
|
+
|
|
74
|
+
confidence = deliberation.fetch(:confidence, DEFAULT_CONFIDENCE + HEURISTIC_BOOST)
|
|
75
|
+
.clamp(CONFIDENCE_FLOOR, CONFIDENCE_CEILING)
|
|
76
|
+
response = deliberation.fetch(:response, :deliberated)
|
|
77
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
78
|
+
|
|
79
|
+
decision = Decision.new(
|
|
80
|
+
query: query,
|
|
81
|
+
domain: domain,
|
|
82
|
+
system_used: :system_two,
|
|
83
|
+
confidence: confidence,
|
|
84
|
+
complexity: deliberation.fetch(:complexity, COMPLEXITY_THRESHOLD),
|
|
85
|
+
effort_cost: EFFORT_COST,
|
|
86
|
+
processing_time_ms: elapsed
|
|
87
|
+
)
|
|
88
|
+
store_decision(decision)
|
|
89
|
+
{ decision_id: decision.id, system: :system_two, response: response, confidence: confidence }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def record_outcome(decision_id:, outcome:)
|
|
93
|
+
decision = @decisions.find { |d| d.id == decision_id }
|
|
94
|
+
return { success: false, reason: :not_found } unless decision
|
|
95
|
+
|
|
96
|
+
decision.outcome = outcome
|
|
97
|
+
|
|
98
|
+
if decision.heuristic_id
|
|
99
|
+
heuristic = @heuristics[decision.heuristic_id]
|
|
100
|
+
heuristic&.use!(success: outcome == :correct)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{ success: true, decision_id: decision_id, outcome: outcome }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def effort_level
|
|
107
|
+
@effort_budget / MAX_EFFORT_BUDGET
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def routing_label
|
|
111
|
+
ROUTING_LABELS.find { |range, _| range.cover?(effort_level) }&.last || :unknown
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def recover_effort
|
|
115
|
+
@effort_budget = (@effort_budget + EFFORT_RECOVERY_RATE).clamp(0.0, MAX_EFFORT_BUDGET)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def decay_heuristics
|
|
119
|
+
@heuristics.each_value do |h|
|
|
120
|
+
next unless h.use_count.positive?
|
|
121
|
+
|
|
122
|
+
delta = -DECAY_RATE
|
|
123
|
+
h.instance_variable_set(:@confidence, (h.confidence + delta).clamp(CONFIDENCE_FLOOR, CONFIDENCE_CEILING))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def system_stats
|
|
128
|
+
s1 = @decisions.count { |d| d.system_used == :system_one }
|
|
129
|
+
s2 = @decisions.count { |d| d.system_used == :system_two }
|
|
130
|
+
total = @decisions.size
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
system_one: s1,
|
|
134
|
+
system_two: s2,
|
|
135
|
+
total: total,
|
|
136
|
+
s1_ratio: total.zero? ? 0.0 : (s1.to_f / total).round(3),
|
|
137
|
+
s2_ratio: total.zero? ? 0.0 : (s2.to_f / total).round(3)
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def best_heuristics(limit: 5)
|
|
142
|
+
@heuristics.values
|
|
143
|
+
.select(&:reliable?)
|
|
144
|
+
.sort_by { |h| [-h.success_rate, -h.use_count] }
|
|
145
|
+
.first(limit)
|
|
146
|
+
.map(&:to_h)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_h
|
|
150
|
+
{
|
|
151
|
+
effort_budget: @effort_budget,
|
|
152
|
+
effort_level: effort_level,
|
|
153
|
+
routing_label: routing_label,
|
|
154
|
+
heuristic_count: @heuristics.size,
|
|
155
|
+
decision_count: @decisions.size,
|
|
156
|
+
system_stats: system_stats
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def find_matching_heuristic(query, domain)
|
|
163
|
+
@heuristics.values
|
|
164
|
+
.select { |h| h.domain == domain || domain.nil? }
|
|
165
|
+
.select { |h| query_matches?(h.pattern, query) }
|
|
166
|
+
.max_by(&:confidence)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def query_matches?(pattern, query)
|
|
170
|
+
case pattern
|
|
171
|
+
when Symbol then query.is_a?(Hash) && query.key?(pattern)
|
|
172
|
+
when String then query.to_s.include?(pattern)
|
|
173
|
+
when Regexp then pattern.match?(query.to_s)
|
|
174
|
+
else false
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def complexity_reason(complexity, matching)
|
|
179
|
+
if complexity >= COMPLEXITY_THRESHOLD
|
|
180
|
+
:high_complexity
|
|
181
|
+
elsif matching.nil?
|
|
182
|
+
:no_heuristic
|
|
183
|
+
else
|
|
184
|
+
:low_confidence
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def store_decision(decision)
|
|
189
|
+
@decisions.shift if @decisions.size >= MAX_DECISIONS
|
|
190
|
+
@decisions << decision
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module DualProcess
|
|
8
|
+
module Helpers
|
|
9
|
+
class Heuristic
|
|
10
|
+
attr_reader :id, :pattern, :domain, :response, :confidence,
|
|
11
|
+
:use_count, :success_count, :created_at, :last_used_at
|
|
12
|
+
|
|
13
|
+
def initialize(pattern:, domain:, response:, confidence: Constants::DEFAULT_CONFIDENCE)
|
|
14
|
+
@id = SecureRandom.uuid
|
|
15
|
+
@pattern = pattern
|
|
16
|
+
@domain = domain
|
|
17
|
+
@response = response
|
|
18
|
+
@confidence = confidence.clamp(Constants::CONFIDENCE_FLOOR, Constants::CONFIDENCE_CEILING)
|
|
19
|
+
@use_count = 0
|
|
20
|
+
@success_count = 0
|
|
21
|
+
@created_at = Time.now.utc
|
|
22
|
+
@last_used_at = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def use!(success: true)
|
|
26
|
+
@use_count += 1
|
|
27
|
+
@last_used_at = Time.now.utc
|
|
28
|
+
@success_count += 1 if success
|
|
29
|
+
|
|
30
|
+
delta = success ? Constants::HEURISTIC_BOOST : -Constants::HEURISTIC_BOOST
|
|
31
|
+
@confidence = (@confidence + delta).clamp(Constants::CONFIDENCE_FLOOR, Constants::CONFIDENCE_CEILING)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success_rate
|
|
35
|
+
return 0.0 if @use_count.zero?
|
|
36
|
+
|
|
37
|
+
@success_count.to_f / @use_count
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reliable?
|
|
41
|
+
success_rate >= 0.7 && @use_count >= 3
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
{
|
|
46
|
+
id: @id,
|
|
47
|
+
pattern: @pattern,
|
|
48
|
+
domain: @domain,
|
|
49
|
+
response: @response,
|
|
50
|
+
confidence: @confidence,
|
|
51
|
+
use_count: @use_count,
|
|
52
|
+
success_count: @success_count,
|
|
53
|
+
success_rate: success_rate,
|
|
54
|
+
reliable: reliable?,
|
|
55
|
+
created_at: @created_at,
|
|
56
|
+
last_used_at: @last_used_at
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DualProcess
|
|
6
|
+
module Runners
|
|
7
|
+
module DualProcess
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def register_heuristic(pattern:, domain:, response:, confidence: nil, **)
|
|
12
|
+
Legion::Logging.debug "[dual_process] register_heuristic pattern=#{pattern} domain=#{domain}"
|
|
13
|
+
opts = { pattern: pattern, domain: domain, response: response }
|
|
14
|
+
opts[:confidence] = confidence unless confidence.nil?
|
|
15
|
+
heuristic = engine.register_heuristic(**opts)
|
|
16
|
+
{ success: true, heuristic: heuristic.to_h }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def route_decision(query:, domain:, complexity:, **)
|
|
20
|
+
Legion::Logging.debug "[dual_process] route_decision domain=#{domain} complexity=#{complexity}"
|
|
21
|
+
route = engine.route_decision(query: query, domain: domain, complexity: complexity)
|
|
22
|
+
{ success: true, **route }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute_system_one(query:, domain:, **)
|
|
26
|
+
Legion::Logging.debug "[dual_process] execute_system_one domain=#{domain}"
|
|
27
|
+
result = engine.execute_system_one(query: query, domain: domain)
|
|
28
|
+
{ success: true, **result }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def execute_system_two(query:, domain:, deliberation: {}, **)
|
|
32
|
+
Legion::Logging.debug "[dual_process] execute_system_two domain=#{domain}"
|
|
33
|
+
result = engine.execute_system_two(query: query, domain: domain, deliberation: deliberation)
|
|
34
|
+
{ success: true, **result }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def record_decision_outcome(decision_id:, outcome:, **)
|
|
38
|
+
Legion::Logging.debug "[dual_process] record_outcome decision_id=#{decision_id} outcome=#{outcome}"
|
|
39
|
+
engine.record_outcome(decision_id: decision_id, outcome: outcome)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def effort_assessment(**)
|
|
43
|
+
Legion::Logging.debug '[dual_process] effort_assessment'
|
|
44
|
+
{
|
|
45
|
+
success: true,
|
|
46
|
+
effort_level: engine.effort_level,
|
|
47
|
+
effort_budget: engine.instance_variable_get(:@effort_budget),
|
|
48
|
+
routing_label: engine.routing_label
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def best_heuristics(limit: 5, **)
|
|
53
|
+
Legion::Logging.debug "[dual_process] best_heuristics limit=#{limit}"
|
|
54
|
+
{ success: true, heuristics: engine.best_heuristics(limit: limit) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def system_usage_stats(**)
|
|
58
|
+
Legion::Logging.debug '[dual_process] system_usage_stats'
|
|
59
|
+
{ success: true, stats: engine.system_stats }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update_dual_process(**)
|
|
63
|
+
Legion::Logging.debug '[dual_process] update_dual_process'
|
|
64
|
+
engine.recover_effort
|
|
65
|
+
engine.decay_heuristics
|
|
66
|
+
{ success: true, effort_level: engine.effort_level, routing_label: engine.routing_label }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dual_process_stats(**)
|
|
70
|
+
Legion::Logging.debug '[dual_process] dual_process_stats'
|
|
71
|
+
{ success: true, **engine.to_h }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def engine
|
|
77
|
+
@engine ||= Helpers::DualProcessEngine.new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/dual_process/version'
|
|
4
|
+
require 'legion/extensions/dual_process/helpers/constants'
|
|
5
|
+
require 'legion/extensions/dual_process/helpers/heuristic'
|
|
6
|
+
require 'legion/extensions/dual_process/helpers/decision'
|
|
7
|
+
require 'legion/extensions/dual_process/helpers/dual_process_engine'
|
|
8
|
+
require 'legion/extensions/dual_process/runners/dual_process'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module DualProcess
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|