activematrix 0.0.5 → 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: 8fd68a9263d0fb04aaa3f2d0d3160b26b995e783ebc12fa0cf9d67c0084e3d5b
4
- data.tar.gz: b282050de6c58f9c470a19f1f17c44f1d6cba0e60b0ee0907e9dd1f82d1395e7
3
+ metadata.gz: 9bf1ad81648f055b2fdb10da688c66ac7267456cea155b0d76bb41864ae6bab5
4
+ data.tar.gz: 282f29e6c278cc4ee5b721039e1ec36c2a48b03ba115e02e9beb017b45211706
5
5
  SHA512:
6
- metadata.gz: 6f90166bf9f0e25db87953d2a79bfec7ddf04542ff975ddf15602f00fbcad5b0f909be3652b85c97ee771e3aab8aca79bb2e01226d35a59c79ba1ae338f2a722
7
- data.tar.gz: 969d6efbe9f3a65294b983410c3737939dca5cb281fa52f55baaa8de317151126e9cc245b1a693398819053bad8e40c7391157f00a55f3835b98d178cd4fa0e7
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
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ module Protocols
5
+ module CS
6
+ # Handles message relationships (replies, edits, reactions, threads)
7
+ # @see https://spec.matrix.org/latest/client-server-api/#relationships
8
+ module MessageRelationships
9
+ # Send a message as a reply to another event
10
+ #
11
+ # @param room_id [String] The room ID
12
+ # @param event_id [String] The event ID to reply to
13
+ # @param content [Hash,String] The message content
14
+ # @param msgtype [String] The message type (defaults to 'm.text')
15
+ # @param params [Hash] Additional parameters for send_message_event
16
+ # @return [Response] The response from the server
17
+ #
18
+ # @example Reply to a message
19
+ # api.reply_to(room_id, event_id, "I agree!")
20
+ #
21
+ # @example Reply with custom msgtype
22
+ # api.reply_to(room_id, event_id, { url: "mxc://..." }, msgtype: 'm.image')
23
+ def reply_to(room_id, event_id, content, msgtype: 'm.text', **params)
24
+ content = { body: content } if content.is_a?(String)
25
+ content[:msgtype] ||= msgtype
26
+
27
+ # Add the reply relationship
28
+ content[:'m.relates_to'] = {
29
+ 'm.in_reply_to' => {
30
+ event_id: event_id
31
+ }
32
+ }
33
+
34
+ send_message_event(room_id, 'm.room.message', content, **params)
35
+ end
36
+
37
+ # Send a threaded message
38
+ #
39
+ # @param room_id [String] The room ID
40
+ # @param thread_root_id [String] The root event ID of the thread
41
+ # @param content [Hash,String] The message content
42
+ # @param msgtype [String] The message type (defaults to 'm.text')
43
+ # @param latest_event_id [String,nil] The latest event in the thread (for fallback)
44
+ # @param include_fallback [Boolean] Include reply fallback for older clients
45
+ # @param params [Hash] Additional parameters for send_message_event
46
+ # @return [Response] The response from the server
47
+ #
48
+ # @example Send a threaded message
49
+ # api.send_threaded_message(room_id, root_event_id, "This is a thread reply")
50
+ #
51
+ # @example Send a threaded message with fallback
52
+ # api.send_threaded_message(room_id, root_id, "Reply", latest_event_id: last_id, include_fallback: true)
53
+ def send_threaded_message(room_id, thread_root_id, content, msgtype: 'm.text',
54
+ latest_event_id: nil, include_fallback: false, **params)
55
+ content = { body: content } if content.is_a?(String)
56
+ content[:msgtype] ||= msgtype
57
+
58
+ # Build the thread relationship
59
+ relates_to = {
60
+ rel_type: 'm.thread',
61
+ event_id: thread_root_id
62
+ }
63
+
64
+ # Add fallback for older clients if requested
65
+ if include_fallback && latest_event_id
66
+ relates_to[:'m.in_reply_to'] = {
67
+ event_id: latest_event_id
68
+ }
69
+ relates_to[:is_falling_back] = true
70
+ end
71
+
72
+ content[:'m.relates_to'] = relates_to
73
+ send_message_event(room_id, 'm.room.message', content, **params)
74
+ end
75
+
76
+ # Edit an existing message
77
+ #
78
+ # @param room_id [String] The room ID
79
+ # @param event_id [String] The event ID to edit
80
+ # @param new_content [Hash,String] The new message content
81
+ # @param msgtype [String] The message type (defaults to 'm.text')
82
+ # @param params [Hash] Additional parameters for send_message_event
83
+ # @return [Response] The response from the server
84
+ #
85
+ # @example Edit a text message
86
+ # api.edit_message(room_id, event_id, "Updated message content")
87
+ #
88
+ # @example Edit with formatted content
89
+ # api.edit_message(room_id, event_id, {
90
+ # body: "Updated *formatted* message",
91
+ # format: "org.matrix.custom.html",
92
+ # formatted_body: "Updated <em>formatted</em> message"
93
+ # })
94
+ def edit_message(room_id, event_id, new_content, msgtype: 'm.text', **params)
95
+ new_content = { body: new_content } if new_content.is_a?(String)
96
+ new_content[:msgtype] ||= msgtype
97
+
98
+ # Build the edit event content
99
+ content = {
100
+ body: "* #{new_content[:body]}", # Fallback with asterisk prefix
101
+ msgtype: msgtype,
102
+ 'm.new_content' => new_content,
103
+ 'm.relates_to' => {
104
+ rel_type: 'm.replace',
105
+ event_id: event_id
106
+ }
107
+ }
108
+
109
+ # Copy format fields to top level if present
110
+ if new_content[:format]
111
+ content[:format] = new_content[:format]
112
+ content[:formatted_body] = "* #{new_content[:formatted_body]}"
113
+ end
114
+
115
+ send_message_event(room_id, 'm.room.message', content, **params)
116
+ end
117
+
118
+ # Send a reaction to an event
119
+ #
120
+ # @param room_id [String] The room ID
121
+ # @param event_id [String] The event ID to react to
122
+ # @param key [String] The reaction key (usually an emoji)
123
+ # @param params [Hash] Additional parameters for send_message_event
124
+ # @return [Response] The response from the server
125
+ #
126
+ # @example React with an emoji
127
+ # api.send_reaction(room_id, event_id, "👍")
128
+ #
129
+ # @example React with custom text
130
+ # api.send_reaction(room_id, event_id, "agree")
131
+ def send_reaction(room_id, event_id, key, **params)
132
+ content = {
133
+ 'm.relates_to' => {
134
+ rel_type: 'm.annotation',
135
+ event_id: event_id,
136
+ key: key
137
+ }
138
+ }
139
+
140
+ send_message_event(room_id, 'm.reaction', content, **params)
141
+ end
142
+
143
+ # Remove a reaction from an event
144
+ #
145
+ # @param room_id [String] The room ID
146
+ # @param reaction_event_id [String] The reaction event ID to redact
147
+ # @param reason [String,nil] Optional reason for removing the reaction
148
+ # @param params [Hash] Additional parameters for redact_event
149
+ # @return [Response] The response from the server
150
+ #
151
+ # @example Remove a reaction
152
+ # api.remove_reaction(room_id, reaction_event_id)
153
+ def remove_reaction(room_id, reaction_event_id, reason: nil, **params)
154
+ redact_event(room_id, reaction_event_id, reason: reason, **params)
155
+ end
156
+
157
+ # Send a reference to another event
158
+ #
159
+ # @param room_id [String] The room ID
160
+ # @param event_id [String] The event ID to reference
161
+ # @param content [Hash,String] The message content
162
+ # @param msgtype [String] The message type (defaults to 'm.text')
163
+ # @param params [Hash] Additional parameters for send_message_event
164
+ # @return [Response] The response from the server
165
+ #
166
+ # @example Send a message referencing another event
167
+ # api.send_reference(room_id, event_id, "See the above message")
168
+ def send_reference(room_id, event_id, content, msgtype: 'm.text', **params)
169
+ content = { body: content } if content.is_a?(String)
170
+ content[:msgtype] ||= msgtype
171
+
172
+ content[:'m.relates_to'] = {
173
+ rel_type: 'm.reference',
174
+ event_id: event_id
175
+ }
176
+
177
+ send_message_event(room_id, 'm.room.message', content, **params)
178
+ end
179
+
180
+ # Get related events for a given event
181
+ #
182
+ # @param room_id [String] The room ID
183
+ # @param event_id [String] The event ID to get relations for
184
+ # @param rel_type [String,nil] Filter by specific relationship type
185
+ # @param event_type [String,nil] Filter by specific event type
186
+ # @param from [String,nil] Pagination token
187
+ # @param to [String,nil] Pagination token
188
+ # @param limit [Integer] Maximum number of events to return
189
+ # @param direction [String] Direction of pagination ('b' for backwards, 'f' for forwards)
190
+ # @param params [Hash] Additional query parameters
191
+ # @return [Response] The related events
192
+ #
193
+ # @example Get all relations
194
+ # api.get_relations(room_id, event_id)
195
+ #
196
+ # @example Get only reactions
197
+ # api.get_relations(room_id, event_id, rel_type: 'm.annotation', event_type: 'm.reaction')
198
+ #
199
+ # @example Get thread replies
200
+ # api.get_relations(room_id, event_id, rel_type: 'm.thread')
201
+ def get_relations(room_id, event_id, rel_type: nil, event_type: nil,
202
+ from: nil, to: nil, limit: nil, direction: 'b', **params)
203
+ query = {
204
+ from: from,
205
+ to: to,
206
+ limit: limit,
207
+ dir: direction
208
+ }.merge(params).compact
209
+
210
+ # Build the appropriate endpoint based on filters
211
+ endpoint = "/rooms/#{room_id}/relations/#{event_id}"
212
+ endpoint += "/#{rel_type}" if rel_type
213
+ endpoint += "/#{event_type}" if rel_type && event_type
214
+
215
+ request(:get, client_api_latest, endpoint, query: query)
216
+ end
217
+
218
+ # Get aggregated relations for multiple events
219
+ #
220
+ # @param room_id [String] The room ID
221
+ # @param event_ids [Array<String>] The event IDs to get relations for
222
+ # @param rel_type [String,nil] Filter by specific relationship type
223
+ # @param event_type [String,nil] Filter by specific event type
224
+ # @param params [Hash] Additional body parameters
225
+ # @return [Response] The aggregated relations
226
+ #
227
+ # @example Get aggregated reactions for multiple events
228
+ # api.get_aggregated_relations(room_id, event_ids, rel_type: 'm.annotation')
229
+ def get_aggregated_relations(room_id, event_ids, rel_type: nil, event_type: nil, **params)
230
+ body = {
231
+ event_ids: event_ids,
232
+ rel_type: rel_type,
233
+ event_type: event_type
234
+ }.merge(params).compact
235
+
236
+ request(:post, client_api_latest, "/rooms/#{room_id}/aggregations", body: body)
237
+ end
238
+
239
+ # Get the edit history of an event
240
+ #
241
+ # @param room_id [String] The room ID
242
+ # @param event_id [String] The event ID to get edit history for
243
+ # @param params [Hash] Additional query parameters
244
+ # @return [Response] The edit history
245
+ #
246
+ # @example Get edit history
247
+ # api.get_edit_history(room_id, event_id)
248
+ def get_edit_history(room_id, event_id, **params)
249
+ get_relations(room_id, event_id, rel_type: 'm.replace', event_type: 'm.room.message', **params)
250
+ end
251
+
252
+ # Get all reactions for an event
253
+ #
254
+ # @param room_id [String] The room ID
255
+ # @param event_id [String] The event ID to get reactions for
256
+ # @param params [Hash] Additional query parameters
257
+ # @return [Response] The reactions
258
+ #
259
+ # @example Get all reactions
260
+ # api.get_reactions(room_id, event_id)
261
+ def get_reactions(room_id, event_id, **params)
262
+ get_relations(room_id, event_id, rel_type: 'm.annotation', event_type: 'm.reaction', **params)
263
+ end
264
+
265
+ # Get thread messages for a root event
266
+ #
267
+ # @param room_id [String] The room ID
268
+ # @param thread_root_id [String] The thread root event ID
269
+ # @param params [Hash] Additional query parameters
270
+ # @return [Response] The thread messages
271
+ #
272
+ # @example Get thread messages
273
+ # api.get_thread_messages(room_id, thread_root_id)
274
+ def get_thread_messages(room_id, thread_root_id, **params)
275
+ get_relations(room_id, thread_root_id, rel_type: 'm.thread', **params)
276
+ end
277
+
278
+ # Check if an event has been edited
279
+ #
280
+ # @param event [Hash] The event to check
281
+ # @return [Boolean] True if the event has been edited
282
+ def event_edited?(event)
283
+ event.dig(:unsigned, :'m.relations', :'m.replace').present?
284
+ end
285
+
286
+ # Get the latest edit content for an event
287
+ #
288
+ # @param event [Hash] The event to get latest content for
289
+ # @return [Hash,nil] The latest content or nil if not edited
290
+ def get_latest_edit_content(event)
291
+ edit_event = event.dig(:unsigned, :'m.relations', :'m.replace')
292
+ return nil unless edit_event
293
+
294
+ # Return the m.new_content if available, otherwise the content
295
+ edit_event.dig(:content, :'m.new_content') || edit_event[:content]
296
+ end
297
+
298
+ # Check if an event is part of a thread
299
+ #
300
+ # @param event [Hash] The event to check
301
+ # @return [Boolean] True if the event is part of a thread
302
+ def in_thread?(event)
303
+ rel_type = event.dig(:content, :'m.relates_to', :rel_type)
304
+ rel_type == 'm.thread'
305
+ end
306
+
307
+ # Get the thread root ID for an event
308
+ #
309
+ # @param event [Hash] The event to get thread root for
310
+ # @return [String,nil] The thread root event ID or nil if not in a thread
311
+ def get_thread_root_id(event)
312
+ return nil unless in_thread?(event)
313
+ event.dig(:content, :'m.relates_to', :event_id)
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
@@ -4,9 +4,17 @@ require 'rails/railtie'
4
4
 
5
5
  module ActiveMatrix
6
6
  class Railtie < Rails::Railtie
7
+ Rails.logger.debug 'ActiveMatrix::Railtie: Loading...'
8
+
7
9
  initializer 'activematrix.configure_rails_initialization' do
10
+ Rails.logger.debug 'ActiveMatrix::Railtie: Initializer running'
8
11
  # Configure Rails.logger as the default logger
9
12
  ActiveMatrix.logger = Rails.logger
13
+ Rails.logger.debug 'ActiveMatrix::Railtie: Logger configured'
14
+
15
+ # Debug autoload paths
16
+ Rails.logger.debug { "ActiveMatrix::Railtie: Autoload paths = #{Rails.application.config.autoload_paths}" }
17
+ Rails.logger.debug { "ActiveMatrix::Railtie: Eager load paths = #{Rails.application.config.eager_load_paths}" }
10
18
  end
11
19
  end
12
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveMatrix
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/active_matrix.rb CHANGED
@@ -5,12 +5,9 @@ require_relative 'active_matrix/version'
5
5
 
6
6
  require 'json'
7
7
  require 'zeitwerk'
8
- require 'active_support'
9
- require 'active_support/core_ext/integer/time'
10
- require 'active_support/core_ext/time/calculations'
11
- require 'active_support/core_ext/time/zones'
12
- require 'active_support/core_ext/hash/keys'
13
- require 'active_support/core_ext/object/blank'
8
+ require 'active_record'
9
+ require 'active_job'
10
+ require 'state_machines-activerecord'
14
11
 
15
12
  module ActiveMatrix
16
13
  # Configuration
@@ -82,7 +79,7 @@ module ActiveMatrix
82
79
  # Ignore directories and files that shouldn't be autoloaded
83
80
  Loader.ignore("#{__dir__}/generators")
84
81
  Loader.ignore("#{__dir__}/activematrix.rb")
85
-
82
+
86
83
  # Ignore files that don't follow Zeitwerk naming conventions
87
84
  Loader.ignore("#{__dir__}/active_matrix/errors.rb")
88
85
  Loader.ignore("#{__dir__}/active_matrix/events.rb")
@@ -107,6 +104,6 @@ module ActiveMatrix
107
104
  require_relative 'active_matrix/events'
108
105
  require_relative 'active_matrix/uri_module'
109
106
 
110
- # Load Railtie for Rails integration
111
- require 'active_matrix/railtie' if defined?(Rails::Railtie)
107
+ # Load Engine for Rails integration
108
+ require 'active_matrix/engine'
112
109
  end
@@ -23,13 +23,6 @@ module ActiveMatrix
23
23
  template 'active_matrix.rb', 'config/initializers/active_matrix.rb'
24
24
  end
25
25
 
26
- def create_models
27
- template 'matrix_agent.rb', 'app/models/matrix_agent.rb'
28
- template 'agent_memory.rb', 'app/models/agent_memory.rb'
29
- template 'conversation_context.rb', 'app/models/conversation_context.rb'
30
- template 'global_memory.rb', 'app/models/global_memory.rb'
31
- end
32
-
33
26
  def display_post_install
34
27
  readme 'README' if behavior == :invoke
35
28
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activematrix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -10,6 +10,20 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: maxitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: mocha
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sqlite3
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: syslog
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +206,20 @@ dependencies:
178
206
  - - "~>"
179
207
  - !ruby/object:Gem::Version
180
208
  version: '3.19'
209
+ - !ruby/object:Gem::Dependency
210
+ name: activejob
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '8.0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '8.0'
181
223
  - !ruby/object:Gem::Dependency
182
224
  name: activerecord
183
225
  requirement: !ruby/object:Gem::Requirement
@@ -238,16 +280,16 @@ dependencies:
238
280
  name: state_machines-activerecord
239
281
  requirement: !ruby/object:Gem::Requirement
240
282
  requirements:
241
- - - "~>"
283
+ - - "<="
242
284
  - !ruby/object:Gem::Version
243
- version: 0.40.0
285
+ version: 0.100.0
244
286
  type: :runtime
245
287
  prerelease: false
246
288
  version_requirements: !ruby/object:Gem::Requirement
247
289
  requirements:
248
- - - "~>"
290
+ - - "<="
249
291
  - !ruby/object:Gem::Version
250
- version: 0.40.0
292
+ version: 0.100.0
251
293
  - !ruby/object:Gem::Dependency
252
294
  name: zeitwerk
253
295
  requirement: !ruby/object:Gem::Requirement
@@ -262,8 +304,15 @@ dependencies:
262
304
  - - "~>"
263
305
  - !ruby/object:Gem::Version
264
306
  version: '2.6'
265
- description: A Ruby on Rails gem that provides seamless integration with the Matrix
266
- protocol, enabling Rails applications to connect and communicate with Matrix servers.
307
+ description: "ActiveMatrix is a comprehensive Rails-native Matrix SDK that enables
308
+ developers to build sophisticated multi-agent bot systems \nand real-time communication
309
+ features. This gem provides deep Rails integration with ActiveRecord models, state
310
+ machines for \nbot lifecycle management, multi-tiered memory systems, intelligent
311
+ event routing, connection pooling, and built-in \ninter-agent communication. Perfect
312
+ for building chatbots, automation systems, monitoring agents, and collaborative
313
+ AI \nsystems within Rails applications. Features include command handling, room
314
+ management, media support, end-to-end encryption \ncapabilities, and extensive protocol
315
+ support (CS, AS, IS, SS).\n"
267
316
  email:
268
317
  - terminale@gmail.com
269
318
  - ace@haxalot.com
@@ -277,6 +326,13 @@ files:
277
326
  - CHANGELOG.md
278
327
  - LICENSE.txt
279
328
  - README.md
329
+ - app/jobs/active_matrix/application_job.rb
330
+ - app/models/active_matrix/agent.rb
331
+ - app/models/active_matrix/agent/jobs/memory_reaper.rb
332
+ - app/models/active_matrix/agent_store.rb
333
+ - app/models/active_matrix/application_record.rb
334
+ - app/models/active_matrix/chat_session.rb
335
+ - app/models/active_matrix/knowledge_base.rb
280
336
  - lib/active_matrix.rb
281
337
  - lib/active_matrix/account_data_cache.rb
282
338
  - lib/active_matrix/agent_manager.rb
@@ -288,6 +344,7 @@ files:
288
344
  - lib/active_matrix/cacheable.rb
289
345
  - lib/active_matrix/client.rb
290
346
  - lib/active_matrix/client_pool.rb
347
+ - lib/active_matrix/engine.rb
291
348
  - lib/active_matrix/errors.rb
292
349
  - lib/active_matrix/event_router.rb
293
350
  - lib/active_matrix/events.rb
@@ -301,6 +358,7 @@ files:
301
358
  - lib/active_matrix/mxid.rb
302
359
  - lib/active_matrix/protocols/as.rb
303
360
  - lib/active_matrix/protocols/cs.rb
361
+ - lib/active_matrix/protocols/cs/message_relationships.rb
304
362
  - lib/active_matrix/protocols/is.rb
305
363
  - lib/active_matrix/protocols/msc.rb
306
364
  - lib/active_matrix/protocols/ss.rb
@@ -319,19 +377,24 @@ files:
319
377
  - lib/generators/active_matrix/install/install_generator.rb
320
378
  - lib/generators/active_matrix/install/templates/README
321
379
  - lib/generators/active_matrix/install/templates/active_matrix.rb
322
- - lib/generators/active_matrix/install/templates/agent_memory.rb
323
- - lib/generators/active_matrix/install/templates/conversation_context.rb
324
380
  - lib/generators/active_matrix/install/templates/create_agent_memories.rb
325
381
  - lib/generators/active_matrix/install/templates/create_conversation_contexts.rb
326
382
  - lib/generators/active_matrix/install/templates/create_global_memories.rb
327
383
  - lib/generators/active_matrix/install/templates/create_matrix_agents.rb
328
- - lib/generators/active_matrix/install/templates/global_memory.rb
329
- - lib/generators/active_matrix/install/templates/matrix_agent.rb
330
384
  homepage: https://github.com/seuros/activematrix
331
385
  licenses:
332
386
  - MIT
333
387
  metadata:
334
388
  rubygems_mfa_required: 'true'
389
+ homepage_uri: https://github.com/seuros/activematrix
390
+ source_code_uri: https://github.com/seuros/activematrix
391
+ changelog_uri: https://github.com/seuros/activematrix/blob/master/CHANGELOG.md
392
+ documentation_uri: https://rubydoc.info/gems/activematrix
393
+ bug_tracker_uri: https://github.com/seuros/activematrix/issues
394
+ wiki_uri: https://github.com/seuros/activematrix/wiki
395
+ tags: matrix, matrix-protocol, matrix-sdk, matrix-api, matrix-client, rails, rails-engine,
396
+ activerecord, activejob, rails-integration, bot, chatbot, multi-agent, agent-system,
397
+ bot-framework, real-time, messaging, communication, chat, state-machine
335
398
  rdoc_options: []
336
399
  require_paths:
337
400
  - lib
@@ -348,5 +411,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
348
411
  requirements: []
349
412
  rubygems_version: 3.6.7
350
413
  specification_version: 4
351
- summary: Rails gem for connecting to Matrix protocol
414
+ summary: Rails-native Matrix SDK for building multi-agent bot systems and real-time
415
+ communication
352
416
  test_files: []
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AgentMemory < ApplicationRecord
4
- belongs_to :matrix_agent
5
-
6
- validates :key, presence: true, uniqueness: { scope: :matrix_agent_id }
7
-
8
- scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
9
- scope :expired, -> { where('expires_at <= ?', Time.current) }
10
-
11
- # Automatically clean up expired memories
12
- after_commit :schedule_cleanup, if: :expires_at?
13
-
14
- def expired?
15
- expires_at.present? && expires_at <= Time.current
16
- end
17
-
18
- def ttl=(seconds)
19
- self.expires_at = seconds.present? ? Time.current + seconds : nil
20
- end
21
-
22
- def ttl
23
- return nil unless expires_at.present?
24
-
25
- remaining = expires_at - Time.current
26
- [remaining, 0].max
27
- end
28
-
29
- # Cache integration
30
- def cache_key
31
- "agent_memory/#{matrix_agent_id}/#{key}"
32
- end
33
-
34
- def write_to_cache
35
- Rails.cache.write(cache_key, value, expires_in: ttl)
36
- end
37
-
38
- def self.cleanup_expired!
39
- expired.destroy_all
40
- end
41
-
42
- private
43
-
44
- def schedule_cleanup
45
- AgentMemoryCleanupJob.set(wait_until: expires_at).perform_later if defined?(AgentMemoryCleanupJob)
46
- end
47
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ConversationContext < ApplicationRecord
4
- belongs_to :matrix_agent
5
-
6
- validates :user_id, presence: true
7
- validates :room_id, presence: true
8
- validates :user_id, uniqueness: { scope: %i[matrix_agent_id room_id] }
9
-
10
- # Configuration
11
- MAX_HISTORY_SIZE = 20
12
-
13
- # Scopes
14
- scope :recent, -> { order(last_message_at: :desc) }
15
- scope :active, -> { where('last_message_at > ?', 1.hour.ago) }
16
- scope :stale, -> { where('last_message_at < ?', 1.day.ago) }
17
-
18
- # Add a message to the history
19
- def add_message(message_data)
20
- messages = message_history['messages'] || []
21
-
22
- # Add new message
23
- messages << {
24
- 'event_id' => message_data[:event_id],
25
- 'sender' => message_data[:sender],
26
- 'content' => message_data[:content],
27
- 'timestamp' => message_data[:timestamp] || Time.current.to_i
28
- }
29
-
30
- # Keep only recent messages
31
- messages = messages.last(MAX_HISTORY_SIZE)
32
-
33
- # Update record
34
- self.message_history = { 'messages' => messages }
35
- self.last_message_at = Time.current
36
- self.message_count = messages.size
37
- save!
38
-
39
- # Update cache
40
- write_to_cache
41
- end
42
-
43
- # Get recent messages
44
- def recent_messages(limit = 10)
45
- messages = message_history['messages'] || []
46
- messages.last(limit)
47
- end
48
-
49
- # Clear old messages but keep context
50
- def prune_history!
51
- messages = message_history['messages'] || []
52
- self.message_history = { 'messages' => messages.last(5) }
53
- save!
54
- end
55
-
56
- # Cache integration
57
- def cache_key
58
- "conversation/#{matrix_agent_id}/#{user_id}/#{room_id}"
59
- end
60
-
61
- def write_to_cache
62
- Rails.cache.write(cache_key, {
63
- context: context,
64
- recent_messages: recent_messages,
65
- last_message_at: last_message_at
66
- }, expires_in: 1.hour)
67
- end
68
-
69
- def self.cleanup_stale!
70
- stale.destroy_all
71
- end
72
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class GlobalMemory < ApplicationRecord
4
- validates :key, presence: true, uniqueness: true
5
-
6
- scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
7
- scope :expired, -> { where('expires_at <= ?', Time.current) }
8
- scope :by_category, ->(category) { where(category: category) }
9
- scope :readable, -> { where(public_read: true) }
10
- scope :writable, -> { where(public_write: true) }
11
-
12
- def expired?
13
- expires_at.present? && expires_at <= Time.current
14
- end
15
-
16
- def readable_by?(agent)
17
- public_read || (agent.is_a?(MatrixAgent) && agent.admin?)
18
- end
19
-
20
- def writable_by?(agent)
21
- public_write || (agent.is_a?(MatrixAgent) && agent.admin?)
22
- end
23
-
24
- # Cache integration
25
- def cache_key
26
- "global/#{key}"
27
- end
28
-
29
- def write_to_cache
30
- return unless active?
31
-
32
- ttl = expires_at.present? ? expires_at - Time.current : nil
33
- Rails.cache.write(cache_key, value, expires_in: ttl)
34
- end
35
-
36
- def self.get(key)
37
- # Try cache first
38
- cached = Rails.cache.read("global/#{key}")
39
- return cached if cached.present?
40
-
41
- # Fallback to database
42
- memory = find_by(key: key)
43
- return unless memory&.active?
44
-
45
- memory.write_to_cache
46
- memory.value
47
- end
48
-
49
- def self.set(key, value, category: nil, expires_in: nil, public_read: true, public_write: false)
50
- memory = find_or_initialize_by(key: key)
51
- memory.value = value
52
- memory.category = category
53
- memory.expires_at = expires_in.present? ? Time.current + expires_in : nil
54
- memory.public_read = public_read
55
- memory.public_write = public_write
56
- memory.save!
57
- memory.write_to_cache
58
- memory
59
- end
60
-
61
- def self.cleanup_expired!
62
- expired.destroy_all
63
- end
64
-
65
- private
66
-
67
- def active?
68
- !expired?
69
- end
70
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class MatrixAgent < ApplicationRecord
4
- # Associations
5
- has_many :agent_memories, dependent: :destroy
6
- has_many :conversation_contexts, dependent: :destroy
7
-
8
- # Validations
9
- validates :name, presence: true, uniqueness: true
10
- validates :homeserver, presence: true
11
- validates :username, presence: true
12
- validates :bot_class, presence: true
13
- validate :valid_bot_class?
14
-
15
- # Scopes
16
- scope :active, -> { where.not(state: %i[offline error]) }
17
- scope :online, -> { where(state: %i[online_idle online_busy]) }
18
- scope :offline, -> { where(state: :offline) }
19
-
20
- # Encrypts password before saving
21
- before_save :encrypt_password, if: :password_changed?
22
-
23
- # State machine for agent lifecycle
24
- state_machine :state, initial: :offline do
25
- state :offline
26
- state :connecting
27
- state :online_idle
28
- state :online_busy
29
- state :error
30
- state :paused
31
-
32
- event :connect do
33
- transition %i[offline error paused] => :connecting
34
- end
35
-
36
- event :connection_established do
37
- transition connecting: :online_idle
38
- end
39
-
40
- after_transition to: :online_idle do |agent|
41
- agent.update(last_active_at: Time.current)
42
- end
43
-
44
- event :start_processing do
45
- transition online_idle: :online_busy
46
- end
47
-
48
- event :finish_processing do
49
- transition online_busy: :online_idle
50
- end
51
-
52
- event :disconnect do
53
- transition %i[connecting online_idle online_busy] => :offline
54
- end
55
-
56
- event :encounter_error do
57
- transition any => :error
58
- end
59
-
60
- event :pause do
61
- transition %i[online_idle online_busy] => :paused
62
- end
63
-
64
- event :resume do
65
- transition paused: :connecting
66
- end
67
- end
68
-
69
- # Instance methods
70
- def bot_instance
71
- @bot_instance ||= bot_class.constantize.new(client) if running?
72
- end
73
-
74
- def client
75
- @client ||= if access_token.present?
76
- ActiveMatrix::Client.new(homeserver, access_token: access_token)
77
- else
78
- ActiveMatrix::Client.new(homeserver)
79
- end
80
- end
81
-
82
- def running?
83
- %i[online_idle online_busy].include?(state.to_sym)
84
- end
85
-
86
- def memory
87
- @memory ||= ActiveMatrix::Memory::AgentMemory.new(self)
88
- end
89
-
90
- def increment_messages_handled!
91
- increment!(:messages_handled)
92
- end
93
-
94
- def update_activity!
95
- update(last_active_at: Time.current)
96
- end
97
-
98
- # Password handling
99
- attr_accessor :password
100
-
101
- def authenticate(password)
102
- return false unless encrypted_password.present?
103
-
104
- BCrypt::Password.new(encrypted_password) == password
105
- end
106
-
107
- private
108
-
109
- def password_changed?
110
- password.present?
111
- end
112
-
113
- def encrypt_password
114
- self.encrypted_password = BCrypt::Password.create(password) if password.present?
115
- end
116
-
117
- def valid_bot_class?
118
- return false if bot_class.blank?
119
-
120
- begin
121
- klass = bot_class.constantize
122
- errors.add(:bot_class, 'must inherit from ActiveMatrix::Bot::Base') unless klass < ActiveMatrix::Bot::Base
123
- rescue NameError
124
- errors.add(:bot_class, 'must be a valid class name')
125
- end
126
- end
127
- end