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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f0db6dac569a30cc870826d01a99877b7f565c70c91807d3e6060dc07af8a9b
4
- data.tar.gz: db8b7398c33b5218e53e46110279854ea8e919315d2c40f70641075765a7f3fd
3
+ metadata.gz: 9bf1ad81648f055b2fdb10da688c66ac7267456cea155b0d76bb41864ae6bab5
4
+ data.tar.gz: 282f29e6c278cc4ee5b721039e1ec36c2a48b03ba115e02e9beb017b45211706
5
5
  SHA512:
6
- metadata.gz: 21bd2769dcce98268e832a2ba4479f38e48e567598c41ab1fc0c18aac73ca448e3805cdc266a4db1b126f3b146e8e2fba70dcf159d635d671f3a39b8e8c73c14
7
- data.tar.gz: edd3589aa53ded85318976e2cedddcba921c187cf831373a764aebc3b190cb65d84a4dcd27e659ec21daecda2b63a4485d87d6975151cc4944aba88771a770f3
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ 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