lex-agentic-memory 0.1.19 → 0.1.20
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 +8 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/client.rb +15 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/helpers/constants.rb +26 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer.rb +157 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb +55 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern/version.rb +13 -0
- data/lib/legion/extensions/agentic/memory/communication_pattern.rb +7 -0
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/lib/legion/extensions/agentic/memory.rb +1 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/constants_spec.rb +36 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb +132 -0
- data/spec/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern_spec.rb +66 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4e20449f626874ab90b96c4452b4b35e4eae22e61df8fb6c9e5a51e4b0b8cd3
|
|
4
|
+
data.tar.gz: eff572db0f462ada591dcfc6e6efa2d8d3092cc43b1be6f907ddcfe9003b0cd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0e21635a796f9d4c9fbb2503446657d727bb01de9bb82a5b6323270611ff13c72a18bb0c520f2fb0ab3a987d5a869ca92ebf7e1e718dce0d20cd0b3712f1722
|
|
7
|
+
data.tar.gz: 0ea2e0a708e2cb8f853bc17595935a596de7e98c03955c9ce97582a7edcdc5546a22e645f9e929b05a08a43e9b52a4e4a1a45329129b1d797cea58d42f833b76
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.20] - 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- CommunicationPattern sub-module for Phase C relational intelligence
|
|
7
|
+
- PatternAnalyzer: time-of-day/day-of-week histograms, channel preference, topic clustering, consistency scoring
|
|
8
|
+
- CommunicationPattern runner: update_patterns, analyze_patterns, pattern_stats
|
|
9
|
+
- Apollo Local persistence for communication pattern state
|
|
10
|
+
|
|
3
11
|
## [0.1.19] - 2026-03-31
|
|
4
12
|
|
|
5
13
|
### Fixed
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Helpers
|
|
9
|
+
module Constants
|
|
10
|
+
HOURS_IN_DAY = 24
|
|
11
|
+
DAYS_IN_WEEK = 7
|
|
12
|
+
|
|
13
|
+
MESSAGE_LENGTH_BUCKETS = %i[short medium long].freeze
|
|
14
|
+
MESSAGE_LENGTH_THRESHOLDS = { short: 50, medium: 200 }.freeze
|
|
15
|
+
|
|
16
|
+
SLIDING_WINDOW_SIZE = 100
|
|
17
|
+
MIN_TRACES_FOR_PATTERN = 10
|
|
18
|
+
|
|
19
|
+
TAG_PREFIX = %w[bond communication_pattern].freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Helpers
|
|
9
|
+
class PatternAnalyzer
|
|
10
|
+
attr_reader :agent_id, :trace_count
|
|
11
|
+
|
|
12
|
+
def initialize(agent_id:)
|
|
13
|
+
@agent_id = agent_id
|
|
14
|
+
@tod_histogram = Array.new(Constants::HOURS_IN_DAY, 0)
|
|
15
|
+
@dow_histogram = Array.new(Constants::DAYS_IN_WEEK, 0)
|
|
16
|
+
@channel_counts = Hash.new(0)
|
|
17
|
+
@direct_count = 0
|
|
18
|
+
@topic_counts = Hash.new(0)
|
|
19
|
+
@trace_count = 0
|
|
20
|
+
@dirty = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_from_traces(traces)
|
|
24
|
+
traces.each { |t| process_trace(t) }
|
|
25
|
+
@dirty = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def time_of_day_distribution
|
|
29
|
+
@tod_histogram.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def day_of_week_distribution
|
|
33
|
+
@dow_histogram.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def channel_preference
|
|
37
|
+
@channel_counts.sort_by { |_k, v| -v }.map(&:first)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def direct_address_frequency
|
|
41
|
+
return 0.0 if @trace_count.zero?
|
|
42
|
+
|
|
43
|
+
@direct_count.to_f / @trace_count
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def topic_clustering
|
|
47
|
+
@topic_counts.dup
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def consistency
|
|
51
|
+
return 0.0 if @trace_count < Constants::MIN_TRACES_FOR_PATTERN
|
|
52
|
+
|
|
53
|
+
channel_entropy = normalized_entropy(@channel_counts.values)
|
|
54
|
+
tod_entropy = normalized_entropy(@tod_histogram)
|
|
55
|
+
(1.0 - ((channel_entropy + tod_entropy) / 2.0)).clamp(0.0, 1.0)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dirty?
|
|
59
|
+
@dirty
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mark_clean!
|
|
63
|
+
@dirty = false
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_apollo_entries
|
|
68
|
+
tags = Constants::TAG_PREFIX.dup + [@agent_id]
|
|
69
|
+
[{ content: serialize(state_hash), tags: tags }]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def from_apollo(store:)
|
|
73
|
+
result = store.query(text: 'communication_pattern',
|
|
74
|
+
tags: Constants::TAG_PREFIX + [@agent_id])
|
|
75
|
+
return false unless result[:success] && result[:results]&.any?
|
|
76
|
+
|
|
77
|
+
parsed = deserialize(result[:results].first[:content])
|
|
78
|
+
return false unless parsed
|
|
79
|
+
|
|
80
|
+
restore_state(parsed)
|
|
81
|
+
true
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
warn "[pattern_analyzer] from_apollo error: #{e.message}"
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def process_trace(trace)
|
|
90
|
+
ts = trace[:created_at]
|
|
91
|
+
ts = Time.parse(ts.to_s) unless ts.is_a?(Time)
|
|
92
|
+
|
|
93
|
+
@tod_histogram[ts.hour] += 1
|
|
94
|
+
@dow_histogram[ts.wday] += 1
|
|
95
|
+
|
|
96
|
+
payload = trace[:content_payload] || {}
|
|
97
|
+
payload = payload.transform_keys(&:to_sym) if payload.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
channel = payload[:channel]&.to_s
|
|
100
|
+
@channel_counts[channel] += 1 if channel
|
|
101
|
+
|
|
102
|
+
@direct_count += 1 if payload[:direct_address]
|
|
103
|
+
|
|
104
|
+
(trace[:domain_tags] || []).each { |tag| @topic_counts[tag.to_s] += 1 }
|
|
105
|
+
|
|
106
|
+
@trace_count += 1
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Legion::Logging.warn("[pattern_analyzer] process_trace error: #{e.message}")
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalized_entropy(counts)
|
|
113
|
+
total = counts.sum.to_f
|
|
114
|
+
return 0.0 if total.zero?
|
|
115
|
+
|
|
116
|
+
probs = counts.select(&:positive?).map { |c| c / total }
|
|
117
|
+
max_entropy = Math.log(probs.size)
|
|
118
|
+
return 0.0 if max_entropy.zero?
|
|
119
|
+
|
|
120
|
+
entropy = -probs.sum { |p| p * Math.log(p) }
|
|
121
|
+
entropy / max_entropy
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def state_hash
|
|
125
|
+
{ agent_id: @agent_id, trace_count: @trace_count,
|
|
126
|
+
tod_histogram: @tod_histogram, dow_histogram: @dow_histogram,
|
|
127
|
+
channel_counts: @channel_counts, direct_count: @direct_count,
|
|
128
|
+
topic_counts: @topic_counts, consistency: consistency }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def restore_state(parsed)
|
|
132
|
+
@trace_count = parsed[:trace_count].to_i
|
|
133
|
+
@tod_histogram = parsed[:tod_histogram] || Array.new(24, 0)
|
|
134
|
+
@dow_histogram = parsed[:dow_histogram] || Array.new(7, 0)
|
|
135
|
+
@channel_counts = Hash.new(0).merge(parsed[:channel_counts] || {})
|
|
136
|
+
@direct_count = parsed[:direct_count].to_i
|
|
137
|
+
@topic_counts = Hash.new(0).merge(parsed[:topic_counts] || {})
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def serialize(hash)
|
|
141
|
+
defined?(Legion::JSON) ? Legion::JSON.dump(hash) : ::JSON.dump(hash)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def deserialize(content)
|
|
145
|
+
parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
|
|
146
|
+
parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : nil
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
Legion::Logging.warn("[pattern_analyzer] deserialize error: #{e.message}")
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Memory
|
|
7
|
+
module CommunicationPattern
|
|
8
|
+
module Runners
|
|
9
|
+
module CommunicationPattern
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
|
+
|
|
13
|
+
def update_patterns(agent_id:, traces: [], **)
|
|
14
|
+
analyzer = analyzer_for(agent_id)
|
|
15
|
+
analyzer.update_from_traces(traces)
|
|
16
|
+
|
|
17
|
+
{ success: true, trace_count: analyzer.trace_count,
|
|
18
|
+
channel_preference: analyzer.channel_preference,
|
|
19
|
+
direct_address_frequency: analyzer.direct_address_frequency,
|
|
20
|
+
consistency: analyzer.consistency }
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
{ success: false, error: e.message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def analyze_patterns(agent_id:, **)
|
|
26
|
+
analyzer = analyzer_for(agent_id)
|
|
27
|
+
{ time_of_day_distribution: analyzer.time_of_day_distribution,
|
|
28
|
+
day_of_week_distribution: analyzer.day_of_week_distribution,
|
|
29
|
+
channel_preference: analyzer.channel_preference,
|
|
30
|
+
direct_address_frequency: analyzer.direct_address_frequency,
|
|
31
|
+
topic_clustering: analyzer.topic_clustering,
|
|
32
|
+
consistency: analyzer.consistency,
|
|
33
|
+
trace_count: analyzer.trace_count }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pattern_stats(agent_id:, **)
|
|
37
|
+
analyzer = analyzer_for(agent_id)
|
|
38
|
+
{ trace_count: analyzer.trace_count,
|
|
39
|
+
channel_preference: analyzer.channel_preference,
|
|
40
|
+
consistency: analyzer.consistency }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def analyzer_for(agent_id)
|
|
46
|
+
@analyzers ||= {}
|
|
47
|
+
@analyzers[agent_id.to_s] ||= Helpers::PatternAnalyzer.new(agent_id: agent_id.to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'communication_pattern/version'
|
|
4
|
+
require_relative 'communication_pattern/helpers/constants'
|
|
5
|
+
require_relative 'communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
require_relative 'communication_pattern/runners/communication_pattern'
|
|
7
|
+
require_relative 'communication_pattern/client'
|
|
@@ -19,6 +19,7 @@ require_relative 'memory/semantic_priming'
|
|
|
19
19
|
require_relative 'memory/semantic_satiation'
|
|
20
20
|
require_relative 'memory/source_monitoring'
|
|
21
21
|
require_relative 'memory/transfer'
|
|
22
|
+
require_relative 'memory/communication_pattern'
|
|
22
23
|
|
|
23
24
|
module Legion
|
|
24
25
|
module Extensions
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Helpers::Constants do
|
|
7
|
+
describe 'HOURS_IN_DAY' do
|
|
8
|
+
it 'is 24' do
|
|
9
|
+
expect(described_class::HOURS_IN_DAY).to eq(24)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe 'DAYS_IN_WEEK' do
|
|
14
|
+
it 'is 7' do
|
|
15
|
+
expect(described_class::DAYS_IN_WEEK).to eq(7)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'MESSAGE_LENGTH_BUCKETS' do
|
|
20
|
+
it 'defines 3 buckets' do
|
|
21
|
+
expect(described_class::MESSAGE_LENGTH_BUCKETS).to eq(%i[short medium long])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'SLIDING_WINDOW_SIZE' do
|
|
26
|
+
it 'is 100' do
|
|
27
|
+
expect(described_class::SLIDING_WINDOW_SIZE).to eq(100)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'MIN_TRACES_FOR_PATTERN' do
|
|
32
|
+
it 'is 10' do
|
|
33
|
+
expect(described_class::MIN_TRACES_FOR_PATTERN).to eq(10)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Helpers::PatternAnalyzer do
|
|
8
|
+
subject(:analyzer) { described_class.new(agent_id: 'partner-1') }
|
|
9
|
+
|
|
10
|
+
let(:trace_9am) do
|
|
11
|
+
{ trace_id: 'tr1', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 9, 0),
|
|
12
|
+
content_payload: { channel: 'teams', direct_address: true, agent_id: 'partner-1' },
|
|
13
|
+
domain_tags: %w[partner observation], source_agent_id: 'partner-1' }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:trace_2pm) do
|
|
17
|
+
{ trace_id: 'tr2', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 14, 0),
|
|
18
|
+
content_payload: { channel: 'cli', direct_address: false, agent_id: 'partner-1' },
|
|
19
|
+
domain_tags: %w[partner observation work], source_agent_id: 'partner-1' }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#update_from_traces' do
|
|
23
|
+
it 'processes an array of traces' do
|
|
24
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
25
|
+
expect(analyzer.trace_count).to eq(2)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'marks dirty' do
|
|
29
|
+
analyzer.update_from_traces([trace_9am])
|
|
30
|
+
expect(analyzer).to be_dirty
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#time_of_day_distribution' do
|
|
35
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
36
|
+
|
|
37
|
+
it 'returns 24-bucket histogram' do
|
|
38
|
+
dist = analyzer.time_of_day_distribution
|
|
39
|
+
expect(dist.size).to eq(24)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'has counts in the right buckets' do
|
|
43
|
+
dist = analyzer.time_of_day_distribution
|
|
44
|
+
expect(dist[9]).to eq(1)
|
|
45
|
+
expect(dist[14]).to eq(1)
|
|
46
|
+
expect(dist[0]).to eq(0)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#day_of_week_distribution' do
|
|
51
|
+
before { analyzer.update_from_traces([trace_9am]) }
|
|
52
|
+
|
|
53
|
+
it 'returns 7-bucket histogram' do
|
|
54
|
+
dist = analyzer.day_of_week_distribution
|
|
55
|
+
expect(dist.size).to eq(7)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#channel_preference' do
|
|
60
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm, trace_9am.merge(trace_id: 'tr3')]) }
|
|
61
|
+
|
|
62
|
+
it 'ranks channels by frequency' do
|
|
63
|
+
prefs = analyzer.channel_preference
|
|
64
|
+
expect(prefs.first).to eq('teams')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#direct_address_frequency' do
|
|
69
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
70
|
+
|
|
71
|
+
it 'computes ratio of direct address traces' do
|
|
72
|
+
ratio = analyzer.direct_address_frequency
|
|
73
|
+
expect(ratio).to be_within(0.01).of(0.5)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#topic_clustering' do
|
|
78
|
+
before { analyzer.update_from_traces([trace_9am, trace_2pm]) }
|
|
79
|
+
|
|
80
|
+
it 'returns domain tag frequencies' do
|
|
81
|
+
topics = analyzer.topic_clustering
|
|
82
|
+
expect(topics).to have_key('partner')
|
|
83
|
+
expect(topics['partner']).to eq(2)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#consistency' do
|
|
88
|
+
it 'returns 0.0 with no data' do
|
|
89
|
+
expect(analyzer.consistency).to eq(0.0)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'returns positive value with data' do
|
|
93
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
94
|
+
expect(analyzer.consistency).to eq(0.0)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe 'Apollo persistence' do
|
|
99
|
+
describe '#to_apollo_entries' do
|
|
100
|
+
before { analyzer.update_from_traces([trace_9am]) }
|
|
101
|
+
|
|
102
|
+
it 'returns entries with correct tags' do
|
|
103
|
+
entries = analyzer.to_apollo_entries
|
|
104
|
+
expect(entries.first[:tags]).to include('bond', 'communication_pattern', 'partner-1')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#from_apollo' do
|
|
109
|
+
let(:mock_store) { double('apollo_local') }
|
|
110
|
+
|
|
111
|
+
it 'restores state' do
|
|
112
|
+
analyzer.update_from_traces([trace_9am, trace_2pm])
|
|
113
|
+
content = analyzer.send(:serialize, analyzer.send(:state_hash))
|
|
114
|
+
|
|
115
|
+
new_analyzer = described_class.new(agent_id: 'partner-1')
|
|
116
|
+
allow(mock_store).to receive(:query)
|
|
117
|
+
.and_return({ success: true, results: [{ content: content }] })
|
|
118
|
+
|
|
119
|
+
expect(new_analyzer.from_apollo(store: mock_store)).to be true
|
|
120
|
+
expect(new_analyzer.trace_count).to eq(2)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe '#mark_clean!' do
|
|
125
|
+
it 'clears dirty flag' do
|
|
126
|
+
analyzer.update_from_traces([trace_9am])
|
|
127
|
+
analyzer.mark_clean!
|
|
128
|
+
expect(analyzer).not_to be_dirty
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer'
|
|
6
|
+
require 'legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern'
|
|
7
|
+
|
|
8
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::CommunicationPattern::Runners::CommunicationPattern do
|
|
9
|
+
let(:host) { Object.new.extend(described_module) }
|
|
10
|
+
let(:described_module) { described_class }
|
|
11
|
+
|
|
12
|
+
before { host.instance_variable_set(:@analyzers, nil) }
|
|
13
|
+
|
|
14
|
+
describe '#update_patterns' do
|
|
15
|
+
let(:traces) do
|
|
16
|
+
[
|
|
17
|
+
{ trace_id: 'tr1', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 9, 0),
|
|
18
|
+
content_payload: { channel: 'teams', direct_address: true }, domain_tags: %w[partner],
|
|
19
|
+
source_agent_id: 'partner-1' },
|
|
20
|
+
{ trace_id: 'tr2', trace_type: :episodic, created_at: Time.utc(2026, 3, 31, 14, 0),
|
|
21
|
+
content_payload: { channel: 'cli', direct_address: false }, domain_tags: %w[partner],
|
|
22
|
+
source_agent_id: 'partner-1' }
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'processes traces and returns result' do
|
|
27
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
28
|
+
expect(result[:success]).to be true
|
|
29
|
+
expect(result[:trace_count]).to eq(2)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns channel preference' do
|
|
33
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
34
|
+
expect(result[:channel_preference]).to be_an(Array)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns direct address frequency' do
|
|
38
|
+
result = host.update_patterns(agent_id: 'partner-1', traces: traces)
|
|
39
|
+
expect(result[:direct_address_frequency]).to be_within(0.01).of(0.5)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#analyze_patterns' do
|
|
44
|
+
it 'returns current patterns for an agent' do
|
|
45
|
+
host.update_patterns(agent_id: 'p1', traces: [
|
|
46
|
+
{ trace_id: 't1', trace_type: :episodic, created_at: Time.now.utc,
|
|
47
|
+
content_payload: { channel: 'teams' }, domain_tags: [], source_agent_id: 'p1' }
|
|
48
|
+
])
|
|
49
|
+
result = host.analyze_patterns(agent_id: 'p1')
|
|
50
|
+
expect(result).to have_key(:time_of_day_distribution)
|
|
51
|
+
expect(result).to have_key(:channel_preference)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns empty result for unknown agent' do
|
|
55
|
+
result = host.analyze_patterns(agent_id: 'unknown')
|
|
56
|
+
expect(result[:trace_count]).to eq(0)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#pattern_stats' do
|
|
61
|
+
it 'returns stats hash' do
|
|
62
|
+
result = host.pattern_stats(agent_id: 'p1')
|
|
63
|
+
expect(result).to have_key(:trace_count)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-agentic-memory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -199,6 +199,12 @@ files:
|
|
|
199
199
|
- lib/legion/extensions/agentic/memory/archaeology/helpers/excavation_site.rb
|
|
200
200
|
- lib/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology.rb
|
|
201
201
|
- lib/legion/extensions/agentic/memory/archaeology/version.rb
|
|
202
|
+
- lib/legion/extensions/agentic/memory/communication_pattern.rb
|
|
203
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/client.rb
|
|
204
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/helpers/constants.rb
|
|
205
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer.rb
|
|
206
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern.rb
|
|
207
|
+
- lib/legion/extensions/agentic/memory/communication_pattern/version.rb
|
|
202
208
|
- lib/legion/extensions/agentic/memory/compression.rb
|
|
203
209
|
- lib/legion/extensions/agentic/memory/compression/actors/maintenance.rb
|
|
204
210
|
- lib/legion/extensions/agentic/memory/compression/client.rb
|
|
@@ -363,6 +369,9 @@ files:
|
|
|
363
369
|
- spec/legion/extensions/agentic/memory/archaeology/helpers/constants_spec.rb
|
|
364
370
|
- spec/legion/extensions/agentic/memory/archaeology/helpers/excavation_site_spec.rb
|
|
365
371
|
- spec/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology_spec.rb
|
|
372
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/helpers/constants_spec.rb
|
|
373
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/helpers/pattern_analyzer_spec.rb
|
|
374
|
+
- spec/legion/extensions/agentic/memory/communication_pattern/runners/communication_pattern_spec.rb
|
|
366
375
|
- spec/legion/extensions/agentic/memory/compression/actors/maintenance_spec.rb
|
|
367
376
|
- spec/legion/extensions/agentic/memory/compression/helpers/compression_engine_spec.rb
|
|
368
377
|
- spec/legion/extensions/agentic/memory/compression/helpers/constants_spec.rb
|