activematrix 0.0.4 → 0.0.7
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/app/jobs/active_matrix/application_job.rb +11 -0
- data/app/models/active_matrix/agent/jobs/memory_reaper.rb +87 -0
- data/app/models/active_matrix/agent.rb +131 -0
- data/app/models/active_matrix/agent_store.rb +51 -0
- data/app/models/active_matrix/application_record.rb +7 -0
- data/app/models/active_matrix/chat_session.rb +76 -0
- data/app/models/active_matrix/knowledge_base.rb +74 -0
- data/lib/active_matrix/engine.rb +14 -0
- data/lib/active_matrix/protocols/cs/message_relationships.rb +318 -0
- data/lib/active_matrix/railtie.rb +8 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +11 -9
- data/lib/generators/active_matrix/install/install_generator.rb +0 -7
- metadata +77 -13
- data/lib/generators/active_matrix/install/templates/agent_memory.rb +0 -47
- data/lib/generators/active_matrix/install/templates/conversation_context.rb +0 -72
- data/lib/generators/active_matrix/install/templates/global_memory.rb +0 -70
- data/lib/generators/active_matrix/install/templates/matrix_agent.rb +0 -127
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bf1ad81648f055b2fdb10da688c66ac7267456cea155b0d76bb41864ae6bab5
|
4
|
+
data.tar.gz: 282f29e6c278cc4ee5b721039e1ec36c2a48b03ba115e02e9beb017b45211706
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08aedb897355151e41f5db2b169d58d0b2842b0c5b7c51a596708ab8ce60be886cb99550f7bb7dfdbf1e71fae918014489d4c48d4271077b930765f6513e28b6'
|
7
|
+
data.tar.gz: b6a3e216fecc58cf8416bc84980c7e5c17edff2b971a2707ee9f82740c69518b60a77880440522765f4befc8a1fc2fc7bb81df39e8fc120c7ddc4451c4368908
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class ApplicationJob < ActiveJob::Base
|
5
|
+
# Automatically retry jobs that encountered a deadlock
|
6
|
+
# retry_on ActiveRecord::Deadlocked
|
7
|
+
|
8
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
9
|
+
# discard_on ActiveJob::DeserializationError
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class Agent < ApplicationRecord
|
5
|
+
module Jobs
|
6
|
+
# Background job responsible for harvesting dead agent memories from the system.
|
7
|
+
#
|
8
|
+
# This job systematically harvests dead memory entries to prevent database bloat
|
9
|
+
# and maintain optimal performance. It operates as a scheduled reaper process
|
10
|
+
# that runs automatically when agent memories reach their expiration time.
|
11
|
+
#
|
12
|
+
# The job performs the following operations:
|
13
|
+
# 1. Identifies dead agent memory records based on their expires_at timestamp
|
14
|
+
# 2. Harvests dead entries from both database and cache layers
|
15
|
+
# 3. Logs harvesting statistics for monitoring and debugging purposes
|
16
|
+
# 4. Handles harvesting failures gracefully without affecting system stability
|
17
|
+
#
|
18
|
+
# Usage:
|
19
|
+
# # Schedule immediate harvesting
|
20
|
+
# ActiveMatrix::Agent::Jobs::MemoryReaper.perform_later
|
21
|
+
#
|
22
|
+
# # Schedule harvesting for specific time
|
23
|
+
# ActiveMatrix::Agent::Jobs::MemoryReaper.set(wait_until: 1.hour.from_now).perform_later
|
24
|
+
#
|
25
|
+
class MemoryReaper < ActiveMatrix::ApplicationJob
|
26
|
+
queue_as :maintenance
|
27
|
+
|
28
|
+
# Performs the memory reaping operation with comprehensive error handling
|
29
|
+
def perform
|
30
|
+
ActiveMatrix.logger.info 'Starting agent memory reaping operation'
|
31
|
+
|
32
|
+
reaping_stats = {
|
33
|
+
agent_memories_reaped: 0,
|
34
|
+
cache_entries_cleared: 0,
|
35
|
+
errors_encountered: 0
|
36
|
+
}
|
37
|
+
|
38
|
+
begin
|
39
|
+
# Harvest dead agent memories
|
40
|
+
reaping_stats[:agent_memories_reaped] = harvest_dead_agent_memories
|
41
|
+
|
42
|
+
# Clear associated cache entries
|
43
|
+
reaping_stats[:cache_entries_cleared] = clear_expired_cache_entries
|
44
|
+
|
45
|
+
ActiveMatrix.logger.info "Memory reaping completed successfully: #{reaping_stats}"
|
46
|
+
rescue StandardError => e
|
47
|
+
reaping_stats[:errors_encountered] += 1
|
48
|
+
ActiveMatrix.logger.error "Memory reaping failed: #{e.message}"
|
49
|
+
ActiveMatrix.logger.error e.backtrace.join("\n")
|
50
|
+
|
51
|
+
# Re-raise to ensure job is marked as failed for retry
|
52
|
+
raise e
|
53
|
+
end
|
54
|
+
|
55
|
+
reaping_stats
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Harvests dead agent memory records from the database
|
61
|
+
# Returns the number of records harvested
|
62
|
+
def harvest_dead_agent_memories
|
63
|
+
return 0 unless defined?(ActiveMatrix::AgentStore)
|
64
|
+
|
65
|
+
dead_memories = ActiveMatrix::AgentStore.expired
|
66
|
+
count = dead_memories.count
|
67
|
+
|
68
|
+
if count.positive?
|
69
|
+
ActiveMatrix.logger.debug "Harvesting #{count} dead agent memory records"
|
70
|
+
dead_memories.destroy_all
|
71
|
+
end
|
72
|
+
|
73
|
+
count
|
74
|
+
end
|
75
|
+
|
76
|
+
# Clears expired memory entries from the Rails cache
|
77
|
+
# Returns the number of cache entries cleared
|
78
|
+
def clear_expired_cache_entries
|
79
|
+
# NOTE: Rails.cache doesn't provide a direct way to clear expired entries
|
80
|
+
# This is a placeholder for cache-specific cleanup logic if needed
|
81
|
+
# Most cache stores handle expiration automatically
|
82
|
+
0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class Agent < ApplicationRecord
|
5
|
+
self.table_name = 'active_matrix_agents'
|
6
|
+
|
7
|
+
# Associations
|
8
|
+
has_many :agent_stores, class_name: 'ActiveMatrix::AgentStore', dependent: :destroy
|
9
|
+
has_many :chat_sessions, class_name: 'ActiveMatrix::ChatSession', dependent: :destroy
|
10
|
+
|
11
|
+
# Validations
|
12
|
+
validates :name, presence: true, uniqueness: true
|
13
|
+
validates :homeserver, presence: true
|
14
|
+
validates :username, presence: true
|
15
|
+
validates :bot_class, presence: true
|
16
|
+
validate :valid_bot_class?
|
17
|
+
|
18
|
+
# Scopes
|
19
|
+
scope :active, -> { where.not(state: %i[offline error]) }
|
20
|
+
scope :online, -> { where(state: %i[online_idle online_busy]) }
|
21
|
+
scope :offline, -> { where(state: :offline) }
|
22
|
+
|
23
|
+
# Encrypts password before saving
|
24
|
+
before_save :encrypt_password, if: :password_changed?
|
25
|
+
|
26
|
+
# State machine for agent lifecycle
|
27
|
+
state_machine :state, initial: :offline do
|
28
|
+
state :offline
|
29
|
+
state :connecting
|
30
|
+
state :online_idle
|
31
|
+
state :online_busy
|
32
|
+
state :error
|
33
|
+
state :paused
|
34
|
+
|
35
|
+
event :connect do
|
36
|
+
transition %i[offline error paused] => :connecting
|
37
|
+
end
|
38
|
+
|
39
|
+
event :connection_established do
|
40
|
+
transition connecting: :online_idle
|
41
|
+
end
|
42
|
+
|
43
|
+
after_transition to: :online_idle do |agent|
|
44
|
+
agent.update(last_active_at: Time.current)
|
45
|
+
end
|
46
|
+
|
47
|
+
event :start_processing do
|
48
|
+
transition online_idle: :online_busy
|
49
|
+
end
|
50
|
+
|
51
|
+
event :finish_processing do
|
52
|
+
transition online_busy: :online_idle
|
53
|
+
end
|
54
|
+
|
55
|
+
event :disconnect do
|
56
|
+
transition %i[connecting online_idle online_busy] => :offline
|
57
|
+
end
|
58
|
+
|
59
|
+
event :encounter_error do
|
60
|
+
transition any => :error
|
61
|
+
end
|
62
|
+
|
63
|
+
event :pause do
|
64
|
+
transition %i[online_idle online_busy] => :paused
|
65
|
+
end
|
66
|
+
|
67
|
+
event :resume do
|
68
|
+
transition paused: :connecting
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Instance methods
|
73
|
+
def bot_instance
|
74
|
+
@bot_instance ||= bot_class.constantize.new(client) if running?
|
75
|
+
end
|
76
|
+
|
77
|
+
def client
|
78
|
+
@client ||= if access_token.present?
|
79
|
+
ActiveMatrix::Client.new(homeserver, access_token: access_token)
|
80
|
+
else
|
81
|
+
ActiveMatrix::Client.new(homeserver)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def running?
|
86
|
+
%i[online_idle online_busy].include?(state.to_sym)
|
87
|
+
end
|
88
|
+
|
89
|
+
def memory
|
90
|
+
@memory ||= ActiveMatrix::Memory::AgentMemory.new(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
def increment_messages_handled!
|
94
|
+
update!(messages_handled: messages_handled + 1)
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_activity!
|
98
|
+
update(last_active_at: Time.current)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Password handling
|
102
|
+
attr_accessor :password
|
103
|
+
|
104
|
+
def authenticate(password)
|
105
|
+
return false if encrypted_password.blank?
|
106
|
+
|
107
|
+
BCrypt::Password.new(encrypted_password) == password
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def password_changed?
|
113
|
+
password.present?
|
114
|
+
end
|
115
|
+
|
116
|
+
def encrypt_password
|
117
|
+
self.encrypted_password = BCrypt::Password.create(password) if password.present?
|
118
|
+
end
|
119
|
+
|
120
|
+
def valid_bot_class?
|
121
|
+
return false if bot_class.blank?
|
122
|
+
|
123
|
+
begin
|
124
|
+
klass = bot_class.constantize
|
125
|
+
errors.add(:bot_class, 'must inherit from ActiveMatrix::Bot::Base') unless klass < ActiveMatrix::Bot::Base
|
126
|
+
rescue NameError
|
127
|
+
errors.add(:bot_class, 'must be a valid class name')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class AgentStore < ApplicationRecord
|
5
|
+
self.table_name = 'active_matrix_agent_stores'
|
6
|
+
|
7
|
+
belongs_to :agent, class_name: 'ActiveMatrix::Agent'
|
8
|
+
|
9
|
+
validates :key, presence: true, uniqueness: { scope: :agent_id }
|
10
|
+
|
11
|
+
scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
|
12
|
+
scope :expired, -> { where(expires_at: ..Time.current) }
|
13
|
+
|
14
|
+
# Automatically clean up expired memories
|
15
|
+
after_commit :schedule_cleanup, if: :expires_at?
|
16
|
+
|
17
|
+
def expired?
|
18
|
+
expires_at.present? && expires_at <= Time.current
|
19
|
+
end
|
20
|
+
|
21
|
+
def ttl=(seconds)
|
22
|
+
self.expires_at = seconds.present? ? Time.current + seconds : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def ttl
|
26
|
+
return nil if expires_at.blank?
|
27
|
+
|
28
|
+
remaining = expires_at - Time.current
|
29
|
+
[remaining, 0].max
|
30
|
+
end
|
31
|
+
|
32
|
+
# Cache integration
|
33
|
+
def cache_key
|
34
|
+
"agent_memory/#{agent_id}/#{key}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_to_cache
|
38
|
+
Rails.cache.write(cache_key, value, expires_in: ttl)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.cleanup_expired!
|
42
|
+
expired.destroy_all
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def schedule_cleanup
|
48
|
+
ActiveMatrix::Agent::Jobs::MemoryReaper.set(wait_until: expires_at).perform_later if defined?(ActiveMatrix::Agent::Jobs::MemoryReaper)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class ChatSession < ApplicationRecord
|
5
|
+
self.table_name = 'active_matrix_chat_sessions'
|
6
|
+
|
7
|
+
belongs_to :agent, class_name: 'ActiveMatrix::Agent'
|
8
|
+
|
9
|
+
validates :user_id, presence: true
|
10
|
+
validates :room_id, presence: true
|
11
|
+
validates :user_id, uniqueness: { scope: %i[agent_id room_id] }
|
12
|
+
|
13
|
+
# Configuration
|
14
|
+
MAX_HISTORY_SIZE = 20
|
15
|
+
|
16
|
+
# Scopes
|
17
|
+
scope :recent, -> { order(last_message_at: :desc) }
|
18
|
+
scope :active, -> { where('last_message_at > ?', 1.hour.ago) }
|
19
|
+
scope :stale, -> { where(last_message_at: ...1.day.ago) }
|
20
|
+
|
21
|
+
# Add a message to the history
|
22
|
+
def add_message(message_data)
|
23
|
+
messages = message_history['messages'] || []
|
24
|
+
|
25
|
+
# Add new message
|
26
|
+
messages << {
|
27
|
+
'event_id' => message_data[:event_id],
|
28
|
+
'sender' => message_data[:sender],
|
29
|
+
'content' => message_data[:content],
|
30
|
+
'timestamp' => message_data[:timestamp] || Time.current.to_i
|
31
|
+
}
|
32
|
+
|
33
|
+
# Keep only recent messages
|
34
|
+
messages = messages.last(MAX_HISTORY_SIZE)
|
35
|
+
|
36
|
+
# Update record
|
37
|
+
self.message_history = { 'messages' => messages }
|
38
|
+
self.last_message_at = Time.current
|
39
|
+
self.message_count = messages.size
|
40
|
+
save!
|
41
|
+
|
42
|
+
# Update cache
|
43
|
+
write_to_cache
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get recent messages
|
47
|
+
def recent_messages(limit = 10)
|
48
|
+
messages = message_history['messages'] || []
|
49
|
+
messages.last(limit)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Clear old messages but keep context
|
53
|
+
def prune_history!
|
54
|
+
messages = message_history['messages'] || []
|
55
|
+
self.message_history = { 'messages' => messages.last(5) }
|
56
|
+
save!
|
57
|
+
end
|
58
|
+
|
59
|
+
# Cache integration
|
60
|
+
def cache_key
|
61
|
+
"conversation/#{agent_id}/#{user_id}/#{room_id}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_to_cache
|
65
|
+
Rails.cache.write(cache_key, {
|
66
|
+
context: context,
|
67
|
+
recent_messages: recent_messages,
|
68
|
+
last_message_at: last_message_at
|
69
|
+
}, expires_in: 1.hour)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.cleanup_stale!
|
73
|
+
stale.destroy_all
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
class KnowledgeBase < ApplicationRecord
|
5
|
+
self.table_name = 'active_matrix_knowledge_bases'
|
6
|
+
|
7
|
+
validates :key, presence: true, uniqueness: true
|
8
|
+
|
9
|
+
scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
|
10
|
+
scope :expired, -> { where(expires_at: ..Time.current) }
|
11
|
+
scope :by_category, ->(category) { where(category: category) }
|
12
|
+
scope :readable, -> { where(public_read: true) }
|
13
|
+
scope :writable, -> { where(public_write: true) }
|
14
|
+
|
15
|
+
def expired?
|
16
|
+
expires_at.present? && expires_at <= Time.current
|
17
|
+
end
|
18
|
+
|
19
|
+
def readable_by?(agent)
|
20
|
+
public_read || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
|
21
|
+
end
|
22
|
+
|
23
|
+
def writable_by?(agent)
|
24
|
+
public_write || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Cache integration
|
28
|
+
def cache_key
|
29
|
+
"global/#{key}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def write_to_cache
|
33
|
+
return unless active?
|
34
|
+
|
35
|
+
ttl = expires_at.present? ? expires_at - Time.current : nil
|
36
|
+
Rails.cache.write(cache_key, value, expires_in: ttl)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get(key)
|
40
|
+
# Try cache first
|
41
|
+
cached = Rails.cache.read("global/#{key}")
|
42
|
+
return cached if cached.present?
|
43
|
+
|
44
|
+
# Fallback to database
|
45
|
+
memory = find_by(key: key)
|
46
|
+
return unless memory&.active?
|
47
|
+
|
48
|
+
memory.write_to_cache
|
49
|
+
memory.value
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.set(key, value, category: nil, expires_in: nil, public_read: true, public_write: false)
|
53
|
+
memory = find_or_initialize_by(key: key)
|
54
|
+
memory.value = value
|
55
|
+
memory.category = category
|
56
|
+
memory.expires_at = expires_in.present? ? Time.current + expires_in : nil
|
57
|
+
memory.public_read = public_read
|
58
|
+
memory.public_write = public_write
|
59
|
+
memory.save!
|
60
|
+
memory.write_to_cache
|
61
|
+
memory
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.cleanup_expired!
|
65
|
+
expired.destroy_all
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def active?
|
71
|
+
!expired?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/engine'
|
4
|
+
|
5
|
+
module ActiveMatrix
|
6
|
+
class Engine < Rails::Engine
|
7
|
+
engine_name 'activematrix'
|
8
|
+
|
9
|
+
initializer 'activematrix.configure_logger' do
|
10
|
+
# Configure logger
|
11
|
+
ActiveMatrix.logger = Rails.logger
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|