lex-cognitive-dwell 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/.github/workflows/ci.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +37 -0
- data/CLAUDE.md +79 -0
- data/Gemfile +13 -0
- data/README.md +43 -0
- data/lex-cognitive-dwell.gemspec +31 -0
- data/lib/legion/extensions/cognitive_dwell/client.rb +15 -0
- data/lib/legion/extensions/cognitive_dwell/helpers/constants.rb +61 -0
- data/lib/legion/extensions/cognitive_dwell/helpers/dwell_engine.rb +122 -0
- data/lib/legion/extensions/cognitive_dwell/helpers/dwell_topic.rb +115 -0
- data/lib/legion/extensions/cognitive_dwell/runners/cognitive_dwell.rb +83 -0
- data/lib/legion/extensions/cognitive_dwell/version.rb +9 -0
- data/lib/legion/extensions/cognitive_dwell.rb +16 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '083991382a820bcb5efce7990358f9b62dfe275c50b1192007ecb1dabd8f29ff'
|
|
4
|
+
data.tar.gz: f912d80e94ac82fc7a05521d207f815f7a92641c293834339cab598e0895c4e4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 77353af88cf0948623549061d73903d474e42c2a8536846fb32f5344bf078c62fe547270264669addab356e9704a2d015369ca5855a28c237b20505689b30125
|
|
7
|
+
data.tar.gz: 46191064d7a67ce8ebfc24783140995c4db538cd4eb3ecdf9c1214f0650c5791cf8e2101f1b334015956b469369fa9381f180494d71b46a88035a4f05a76becb
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
10
|
+
|
|
11
|
+
release:
|
|
12
|
+
needs: ci
|
|
13
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
15
|
+
secrets:
|
|
16
|
+
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.4
|
|
4
|
+
|
|
5
|
+
Style/Documentation:
|
|
6
|
+
Enabled: false
|
|
7
|
+
|
|
8
|
+
Naming/PredicateMethod:
|
|
9
|
+
Enabled: false
|
|
10
|
+
|
|
11
|
+
Naming/PredicatePrefix:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Metrics/ClassLength:
|
|
15
|
+
Max: 150
|
|
16
|
+
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 25
|
|
19
|
+
|
|
20
|
+
Metrics/AbcSize:
|
|
21
|
+
Max: 25
|
|
22
|
+
|
|
23
|
+
Metrics/ParameterLists:
|
|
24
|
+
Max: 8
|
|
25
|
+
MaxOptionalParameters: 8
|
|
26
|
+
|
|
27
|
+
Layout/HashAlignment:
|
|
28
|
+
EnforcedColonStyle: table
|
|
29
|
+
EnforcedHashRocketStyle: table
|
|
30
|
+
|
|
31
|
+
Metrics/BlockLength:
|
|
32
|
+
Exclude:
|
|
33
|
+
- 'spec/**/*'
|
|
34
|
+
|
|
35
|
+
Style/OneClassPerFile:
|
|
36
|
+
Exclude:
|
|
37
|
+
- 'spec/spec_helper.rb'
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# lex-cognitive-dwell
|
|
2
|
+
|
|
3
|
+
**Level 3 Documentation**
|
|
4
|
+
- **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
|
|
5
|
+
- **Grandparent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
Models how long the system lingers on topics based on salience, novelty, emotional intensity, and complexity. Detects sticky topics and rumination. Provides attention-as-duration modeling: some topics naturally attract longer dwell times; others are fleeting. Rumination is detected when dwell time reaches critical levels.
|
|
10
|
+
|
|
11
|
+
## Gem Info
|
|
12
|
+
|
|
13
|
+
- **Gem name**: `lex-cognitive-dwell`
|
|
14
|
+
- **Version**: `0.1.0`
|
|
15
|
+
- **Module**: `Legion::Extensions::CognitiveDwell`
|
|
16
|
+
- **Ruby**: `>= 3.4`
|
|
17
|
+
- **License**: MIT
|
|
18
|
+
|
|
19
|
+
## File Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
lib/legion/extensions/cognitive_dwell/
|
|
23
|
+
cognitive_dwell.rb
|
|
24
|
+
version.rb
|
|
25
|
+
client.rb
|
|
26
|
+
helpers/
|
|
27
|
+
constants.rb
|
|
28
|
+
dwell_engine.rb
|
|
29
|
+
dwell_topic.rb
|
|
30
|
+
runners/
|
|
31
|
+
cognitive_dwell.rb
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Key Constants
|
|
35
|
+
|
|
36
|
+
From `helpers/constants.rb`:
|
|
37
|
+
|
|
38
|
+
- `TOPIC_TYPES` — `%i[problem concept conversation task memory emotion plan decision observation]`
|
|
39
|
+
- `MAX_TOPICS` = `200`, `MAX_DWELL_HISTORY` = `500`
|
|
40
|
+
- `BASE_DWELL` = `0.3`
|
|
41
|
+
- Dwell score weights: `SALIENCE_WEIGHT` = `0.25`, `NOVELTY_WEIGHT` = `0.25`, `EMOTION_WEIGHT` = `0.3`, `COMPLEXITY_WEIGHT` = `0.2`
|
|
42
|
+
- `DWELL_DECAY` = `0.05`, `ENGAGEMENT_BOOST` = `0.08`
|
|
43
|
+
- `STICKY_THRESHOLD` = `0.7`, `FLEETING_THRESHOLD` = `0.2`, `RUMINATION_THRESHOLD` = `0.9`
|
|
44
|
+
- `DWELL_LABELS` — `0.8+` = `:stuck`, `0.6` = `:engrossed`, `0.4` = `:attending`, `0.2` = `:browsing`, below = `:fleeting`
|
|
45
|
+
- `ENGAGEMENT_LABELS` — `0.8+` = `:deeply_engaged` through below `0.2` = `:disengaged`
|
|
46
|
+
- `DISENGAGE_LABELS` — `0.8+` = `:very_hard` through below `0.2` = `:effortless`
|
|
47
|
+
|
|
48
|
+
## Runners
|
|
49
|
+
|
|
50
|
+
All methods in `Runners::CognitiveDwell`:
|
|
51
|
+
|
|
52
|
+
- `add_topic(content:, topic_type: :concept, salience: 0.5, novelty: 0.5, emotional_intensity: 0.3, complexity: 0.5)` — adds a topic with initial dwell score computed from weighted inputs
|
|
53
|
+
- `focus_on(topic_id:)` — boosts engagement score on a topic; increases dwell time
|
|
54
|
+
- `disengage(topic_id:, force: 0.0)` — reduces engagement; `force` parameter accelerates disengagement
|
|
55
|
+
- `decay(engine: nil)` — applies dwell decay to all topics; returns decayed count
|
|
56
|
+
- `current_topic` — the topic with the highest current dwell score
|
|
57
|
+
- `sticky_topics` — topics above `STICKY_THRESHOLD` (dwell >= 0.7)
|
|
58
|
+
- `ruminating_topics` — topics above `RUMINATION_THRESHOLD` (dwell >= 0.9)
|
|
59
|
+
- `most_engaging(limit: 5)` — top topics by engagement score
|
|
60
|
+
- `dwell_report` — full report: totals, sticky count, rumination count, average dwell
|
|
61
|
+
- `status` — engine summary
|
|
62
|
+
|
|
63
|
+
## Helpers
|
|
64
|
+
|
|
65
|
+
- `DwellEngine` — manages topics. Dwell score = `BASE_DWELL + (salience * SALIENCE_WEIGHT) + (novelty * NOVELTY_WEIGHT) + (emotional_intensity * EMOTION_WEIGHT) + (complexity * COMPLEXITY_WEIGHT)`. `decay_all!` applies `DWELL_DECAY` to all topics.
|
|
66
|
+
- `DwellTopic` — has `content`, `topic_type`, `dwell_score`, `engagement`, `salience`, `novelty`, `emotional_intensity`, `complexity`. `focus!` boosts engagement. `disengage!(force)` reduces both dwell and engagement.
|
|
67
|
+
|
|
68
|
+
## Integration Points
|
|
69
|
+
|
|
70
|
+
- `lex-cognitive-echo` models residual activation from past processing — dwell is the active attention complement: echo is background, dwell is foreground.
|
|
71
|
+
- `lex-tick` can check `ruminating_topics` in the health phase to detect unhealthy attention loops and trigger defusion or disengagement.
|
|
72
|
+
- Emotional weight (`EMOTION_WEIGHT = 0.3`) is the highest single factor — emotionally intense topics naturally attract longer dwell, modeling the affect-attention link.
|
|
73
|
+
|
|
74
|
+
## Development Notes
|
|
75
|
+
|
|
76
|
+
- `current_topic` returns the single highest-dwell topic — the agent's most attended-to concept at any moment.
|
|
77
|
+
- `RUMINATION_THRESHOLD = 0.9` is deliberately high — rumination is a clinical-level concern, not normal engagement.
|
|
78
|
+
- `disengage(force: 0.0)` with default force is gentle; `force: 1.0` enables hard disengagement override.
|
|
79
|
+
- Dwell decay applies to all topics including the current topic — active engagement requires periodic `focus_on` calls to maintain dwell level.
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# lex-cognitive-dwell
|
|
2
|
+
|
|
3
|
+
Cognitive dwell time modeling for LegionIO. Models how long the system lingers on topics based on salience, novelty, emotional intensity, and complexity. Detects sticky topics and rumination.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Not all topics receive equal attention. This extension models dwell time — how long a topic captures cognitive focus — as a weighted function of salience, novelty, emotional intensity, and complexity. Topics with high emotional intensity (weight 0.3) attract the most attention; salience and novelty each contribute equally (0.25); complexity adds a smaller pull (0.2). Dwell decays over time unless actively reinforced by focus.
|
|
8
|
+
|
|
9
|
+
Three critical thresholds: sticky (0.7, noteworthy persistence), and rumination (0.9, unhealthy fixation requiring intervention).
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
client = Legion::Extensions::CognitiveDwell::Client.new
|
|
15
|
+
|
|
16
|
+
topic = client.add_topic(
|
|
17
|
+
content: 'unresolved question about consent tier promotion criteria',
|
|
18
|
+
topic_type: :problem,
|
|
19
|
+
salience: 0.8,
|
|
20
|
+
novelty: 0.6,
|
|
21
|
+
emotional_intensity: 0.7,
|
|
22
|
+
complexity: 0.5
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
client.focus_on(topic_id: topic[:topic][:id])
|
|
26
|
+
client.sticky_topics
|
|
27
|
+
client.ruminating_topics
|
|
28
|
+
|
|
29
|
+
client.decay # call each tick
|
|
30
|
+
client.dwell_report
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
bundle exec rspec
|
|
38
|
+
bundle exec rubocop
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/cognitive_dwell/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-cognitive-dwell'
|
|
7
|
+
spec.version = Legion::Extensions::CognitiveDwell::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Cognitive dwell time modeling for LegionIO'
|
|
12
|
+
spec.description = 'Models how long the system lingers on topics based on salience, novelty, emotional ' \
|
|
13
|
+
'intensity, and complexity. Detects sticky topics and rumination.'
|
|
14
|
+
spec.homepage = 'https://github.com/LegionIO/lex-cognitive-dwell'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
|
|
17
|
+
spec.required_ruby_version = '>= 3.4'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-dwell'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-dwell/blob/master/README.md'
|
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-dwell/blob/master/CHANGELOG.md'
|
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-dwell/issues'
|
|
24
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.chdir(__dir__) do
|
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
28
|
+
end
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
spec.add_development_dependency 'legion-gaia'
|
|
31
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveDwell
|
|
6
|
+
class Client
|
|
7
|
+
include Runners::CognitiveDwell
|
|
8
|
+
|
|
9
|
+
def initialize(engine: nil)
|
|
10
|
+
@default_engine = engine || Helpers::DwellEngine.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveDwell
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_TOPICS = 200
|
|
9
|
+
MAX_DWELL_HISTORY = 500
|
|
10
|
+
|
|
11
|
+
# Dwell dynamics
|
|
12
|
+
BASE_DWELL = 0.3
|
|
13
|
+
SALIENCE_WEIGHT = 0.25
|
|
14
|
+
NOVELTY_WEIGHT = 0.25
|
|
15
|
+
EMOTION_WEIGHT = 0.3
|
|
16
|
+
COMPLEXITY_WEIGHT = 0.2
|
|
17
|
+
DWELL_DECAY = 0.05
|
|
18
|
+
ENGAGEMENT_BOOST = 0.08
|
|
19
|
+
|
|
20
|
+
# Thresholds
|
|
21
|
+
STICKY_THRESHOLD = 0.7
|
|
22
|
+
FLEETING_THRESHOLD = 0.2
|
|
23
|
+
RUMINATION_THRESHOLD = 0.9
|
|
24
|
+
|
|
25
|
+
# Topic types
|
|
26
|
+
TOPIC_TYPES = %i[
|
|
27
|
+
problem concept conversation task memory
|
|
28
|
+
emotion plan decision observation
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Dwell duration labels
|
|
32
|
+
DWELL_LABELS = {
|
|
33
|
+
(0.8..) => :stuck,
|
|
34
|
+
(0.6...0.8) => :engrossed,
|
|
35
|
+
(0.4...0.6) => :attending,
|
|
36
|
+
(0.2...0.4) => :browsing,
|
|
37
|
+
(..0.2) => :fleeting
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# Engagement labels
|
|
41
|
+
ENGAGEMENT_LABELS = {
|
|
42
|
+
(0.8..) => :deeply_engaged,
|
|
43
|
+
(0.6...0.8) => :engaged,
|
|
44
|
+
(0.4...0.6) => :moderate,
|
|
45
|
+
(0.2...0.4) => :light,
|
|
46
|
+
(..0.2) => :disengaged
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Disengagement difficulty labels
|
|
50
|
+
DISENGAGE_LABELS = {
|
|
51
|
+
(0.8..) => :very_hard,
|
|
52
|
+
(0.6...0.8) => :hard,
|
|
53
|
+
(0.4...0.6) => :moderate,
|
|
54
|
+
(0.2...0.4) => :easy,
|
|
55
|
+
(..0.2) => :effortless
|
|
56
|
+
}.freeze
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveDwell
|
|
6
|
+
module Helpers
|
|
7
|
+
class DwellEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@topics = {}
|
|
12
|
+
@current_topic_id = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_topic(content:, topic_type: :concept, salience: 0.5,
|
|
16
|
+
novelty: 0.5, emotional_intensity: 0.3, complexity: 0.5)
|
|
17
|
+
prune_if_needed
|
|
18
|
+
topic = DwellTopic.new(
|
|
19
|
+
content: content, topic_type: topic_type, salience: salience,
|
|
20
|
+
novelty: novelty, emotional_intensity: emotional_intensity, complexity: complexity
|
|
21
|
+
)
|
|
22
|
+
@topics[topic.id] = topic
|
|
23
|
+
topic
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def focus_on(topic_id:)
|
|
27
|
+
topic = @topics[topic_id]
|
|
28
|
+
return nil unless topic
|
|
29
|
+
|
|
30
|
+
topic.engage!
|
|
31
|
+
@current_topic_id = topic_id
|
|
32
|
+
topic
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def disengage(topic_id:, force: 0.0)
|
|
36
|
+
topic = @topics[topic_id]
|
|
37
|
+
return nil unless topic
|
|
38
|
+
|
|
39
|
+
topic.disengage!(force: force)
|
|
40
|
+
@current_topic_id = nil if @current_topic_id == topic_id
|
|
41
|
+
topic
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def decay_all!
|
|
45
|
+
@topics.each_value(&:decay!)
|
|
46
|
+
{ topics_decayed: @topics.size }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_topic
|
|
50
|
+
@topics[@current_topic_id]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sticky_topics
|
|
54
|
+
@topics.values.select(&:sticky?)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fleeting_topics
|
|
58
|
+
@topics.values.select(&:fleeting?)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ruminating_topics
|
|
62
|
+
@topics.values.select(&:ruminating?)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def most_engaging(limit: 5)
|
|
66
|
+
@topics.values.sort_by { |t| -t.dwell_level }.first(limit)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def hardest_to_disengage(limit: 5)
|
|
70
|
+
@topics.values.sort_by { |t| -t.disengagement_difficulty }.first(limit)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def average_dwell
|
|
74
|
+
return BASE_DWELL if @topics.empty?
|
|
75
|
+
|
|
76
|
+
vals = @topics.values.map(&:dwell_level)
|
|
77
|
+
(vals.sum / vals.size).round(10)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def average_disengagement_difficulty
|
|
81
|
+
return 0.0 if @topics.empty?
|
|
82
|
+
|
|
83
|
+
vals = @topics.values.map(&:disengagement_difficulty)
|
|
84
|
+
(vals.sum / vals.size).round(10)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def dwell_report
|
|
88
|
+
{
|
|
89
|
+
total_topics: @topics.size,
|
|
90
|
+
current_topic: current_topic&.to_h,
|
|
91
|
+
sticky_count: sticky_topics.size,
|
|
92
|
+
fleeting_count: fleeting_topics.size,
|
|
93
|
+
ruminating_count: ruminating_topics.size,
|
|
94
|
+
average_dwell: average_dwell,
|
|
95
|
+
average_disengagement_difficulty: average_disengagement_difficulty,
|
|
96
|
+
most_engaging: most_engaging(limit: 3).map(&:to_h)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_h
|
|
101
|
+
{
|
|
102
|
+
total_topics: @topics.size,
|
|
103
|
+
current_topic_id: @current_topic_id,
|
|
104
|
+
sticky_count: sticky_topics.size,
|
|
105
|
+
ruminating_count: ruminating_topics.size,
|
|
106
|
+
average_dwell: average_dwell
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def prune_if_needed
|
|
113
|
+
return if @topics.size < MAX_TOPICS
|
|
114
|
+
|
|
115
|
+
least_engaged = @topics.values.min_by(&:dwell_level)
|
|
116
|
+
@topics.delete(least_engaged.id) if least_engaged
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module CognitiveDwell
|
|
8
|
+
module Helpers
|
|
9
|
+
class DwellTopic
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :content, :topic_type, :salience, :novelty,
|
|
13
|
+
:emotional_intensity, :complexity, :dwell_level,
|
|
14
|
+
:engagement_count, :created_at
|
|
15
|
+
|
|
16
|
+
def initialize(content:, topic_type: :concept, salience: 0.5,
|
|
17
|
+
novelty: 0.5, emotional_intensity: 0.3, complexity: 0.5)
|
|
18
|
+
@id = SecureRandom.uuid
|
|
19
|
+
@content = content
|
|
20
|
+
@topic_type = topic_type.to_sym
|
|
21
|
+
@salience = salience.to_f.clamp(0.0, 1.0).round(10)
|
|
22
|
+
@novelty = novelty.to_f.clamp(0.0, 1.0).round(10)
|
|
23
|
+
@emotional_intensity = emotional_intensity.to_f.clamp(0.0, 1.0).round(10)
|
|
24
|
+
@complexity = complexity.to_f.clamp(0.0, 1.0).round(10)
|
|
25
|
+
@dwell_level = compute_initial_dwell
|
|
26
|
+
@engagement_count = 0
|
|
27
|
+
@created_at = Time.now.utc
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def engage!
|
|
31
|
+
@engagement_count += 1
|
|
32
|
+
@dwell_level = (@dwell_level + ENGAGEMENT_BOOST).clamp(0.0, 1.0).round(10)
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def decay!
|
|
37
|
+
@dwell_level = (@dwell_level - DWELL_DECAY).clamp(0.0, 1.0).round(10)
|
|
38
|
+
@novelty = (@novelty - 0.02).clamp(0.0, 1.0).round(10)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def disengage!(force: 0.0)
|
|
43
|
+
reduction = (0.1 + force).clamp(0.0, 1.0)
|
|
44
|
+
@dwell_level = (@dwell_level - reduction).clamp(0.0, 1.0).round(10)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sticky?
|
|
49
|
+
@dwell_level >= STICKY_THRESHOLD
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fleeting?
|
|
53
|
+
@dwell_level <= FLEETING_THRESHOLD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ruminating?
|
|
57
|
+
@dwell_level >= RUMINATION_THRESHOLD
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def disengagement_difficulty
|
|
61
|
+
((@emotional_intensity * 0.4) + (@dwell_level * 0.4) + (@complexity * 0.2)).round(10)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dwell_label
|
|
65
|
+
match = DWELL_LABELS.find { |range, _| range.cover?(@dwell_level) }
|
|
66
|
+
match ? match.last : :fleeting
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def engagement_label
|
|
70
|
+
score = ((@dwell_level * 0.6) + (@salience * 0.4)).round(10)
|
|
71
|
+
match = ENGAGEMENT_LABELS.find { |range, _| range.cover?(score) }
|
|
72
|
+
match ? match.last : :disengaged
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def disengage_label
|
|
76
|
+
match = DISENGAGE_LABELS.find { |range, _| range.cover?(disengagement_difficulty) }
|
|
77
|
+
match ? match.last : :effortless
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_h
|
|
81
|
+
{
|
|
82
|
+
id: @id,
|
|
83
|
+
content: @content,
|
|
84
|
+
topic_type: @topic_type,
|
|
85
|
+
salience: @salience,
|
|
86
|
+
novelty: @novelty,
|
|
87
|
+
emotional_intensity: @emotional_intensity,
|
|
88
|
+
complexity: @complexity,
|
|
89
|
+
dwell_level: @dwell_level,
|
|
90
|
+
dwell_label: dwell_label,
|
|
91
|
+
sticky: sticky?,
|
|
92
|
+
fleeting: fleeting?,
|
|
93
|
+
ruminating: ruminating?,
|
|
94
|
+
disengagement_difficulty: disengagement_difficulty,
|
|
95
|
+
disengage_label: disengage_label,
|
|
96
|
+
engagement_count: @engagement_count,
|
|
97
|
+
engagement_label: engagement_label,
|
|
98
|
+
created_at: @created_at
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def compute_initial_dwell
|
|
105
|
+
(BASE_DWELL +
|
|
106
|
+
(@salience * SALIENCE_WEIGHT) +
|
|
107
|
+
(@novelty * NOVELTY_WEIGHT) +
|
|
108
|
+
(@emotional_intensity * EMOTION_WEIGHT) +
|
|
109
|
+
(@complexity * COMPLEXITY_WEIGHT)).clamp(0.0, 1.0).round(10)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveDwell
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveDwell
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
9
|
+
|
|
10
|
+
def add_topic(content:, topic_type: :concept, salience: 0.5,
|
|
11
|
+
novelty: 0.5, emotional_intensity: 0.3, complexity: 0.5, engine: nil, **)
|
|
12
|
+
eng = engine || default_engine
|
|
13
|
+
topic = eng.add_topic(content: content, topic_type: topic_type, salience: salience,
|
|
14
|
+
novelty: novelty, emotional_intensity: emotional_intensity,
|
|
15
|
+
complexity: complexity)
|
|
16
|
+
{ success: true, topic: topic.to_h }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def focus_on(topic_id:, engine: nil, **)
|
|
20
|
+
eng = engine || default_engine
|
|
21
|
+
topic = eng.focus_on(topic_id: topic_id)
|
|
22
|
+
return { success: false, error: 'topic not found' } unless topic
|
|
23
|
+
|
|
24
|
+
{ success: true, topic: topic.to_h }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def disengage(topic_id:, force: 0.0, engine: nil, **)
|
|
28
|
+
eng = engine || default_engine
|
|
29
|
+
topic = eng.disengage(topic_id: topic_id, force: force)
|
|
30
|
+
return { success: false, error: 'topic not found' } unless topic
|
|
31
|
+
|
|
32
|
+
{ success: true, topic: topic.to_h }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def decay(engine: nil, **)
|
|
36
|
+
eng = engine || default_engine
|
|
37
|
+
result = eng.decay_all!
|
|
38
|
+
{ success: true, **result }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_topic(engine: nil, **)
|
|
42
|
+
eng = engine || default_engine
|
|
43
|
+
topic = eng.current_topic
|
|
44
|
+
return { success: false, error: 'no current topic' } unless topic
|
|
45
|
+
|
|
46
|
+
{ success: true, topic: topic.to_h }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sticky_topics(engine: nil, **)
|
|
50
|
+
eng = engine || default_engine
|
|
51
|
+
{ success: true, topics: eng.sticky_topics.map(&:to_h) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ruminating_topics(engine: nil, **)
|
|
55
|
+
eng = engine || default_engine
|
|
56
|
+
{ success: true, topics: eng.ruminating_topics.map(&:to_h) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def most_engaging(limit: 5, engine: nil, **)
|
|
60
|
+
eng = engine || default_engine
|
|
61
|
+
{ success: true, topics: eng.most_engaging(limit: limit).map(&:to_h) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dwell_report(engine: nil, **)
|
|
65
|
+
eng = engine || default_engine
|
|
66
|
+
{ success: true, report: eng.dwell_report }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def status(engine: nil, **)
|
|
70
|
+
eng = engine || default_engine
|
|
71
|
+
{ success: true, **eng.to_h }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def default_engine
|
|
77
|
+
@default_engine ||= Helpers::DwellEngine.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_relative 'cognitive_dwell/version'
|
|
4
|
+
require_relative 'cognitive_dwell/helpers/constants'
|
|
5
|
+
require_relative 'cognitive_dwell/helpers/dwell_topic'
|
|
6
|
+
require_relative 'cognitive_dwell/helpers/dwell_engine'
|
|
7
|
+
require_relative 'cognitive_dwell/runners/cognitive_dwell'
|
|
8
|
+
require_relative 'cognitive_dwell/client'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module CognitiveDwell
|
|
13
|
+
extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cognitive-dwell
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
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: Models how long the system lingers on topics based on salience, novelty,
|
|
27
|
+
emotional intensity, and complexity. Detects sticky topics and rumination.
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".github/workflows/ci.yml"
|
|
35
|
+
- ".gitignore"
|
|
36
|
+
- ".rspec"
|
|
37
|
+
- ".rubocop.yml"
|
|
38
|
+
- CLAUDE.md
|
|
39
|
+
- Gemfile
|
|
40
|
+
- README.md
|
|
41
|
+
- lex-cognitive-dwell.gemspec
|
|
42
|
+
- lib/legion/extensions/cognitive_dwell.rb
|
|
43
|
+
- lib/legion/extensions/cognitive_dwell/client.rb
|
|
44
|
+
- lib/legion/extensions/cognitive_dwell/helpers/constants.rb
|
|
45
|
+
- lib/legion/extensions/cognitive_dwell/helpers/dwell_engine.rb
|
|
46
|
+
- lib/legion/extensions/cognitive_dwell/helpers/dwell_topic.rb
|
|
47
|
+
- lib/legion/extensions/cognitive_dwell/runners/cognitive_dwell.rb
|
|
48
|
+
- lib/legion/extensions/cognitive_dwell/version.rb
|
|
49
|
+
homepage: https://github.com/LegionIO/lex-cognitive-dwell
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-dwell
|
|
54
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-dwell
|
|
55
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-dwell/blob/master/README.md
|
|
56
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-dwell/blob/master/CHANGELOG.md
|
|
57
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-dwell/issues
|
|
58
|
+
rubygems_mfa_required: 'true'
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.4'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.9
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Cognitive dwell time modeling for LegionIO
|
|
76
|
+
test_files: []
|