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 +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 +6 -9
- data/lib/generators/active_matrix/install/install_generator.rb +0 -7
- metadata +76 -12
- 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
|
@@ -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
|
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 '
|
9
|
-
require '
|
10
|
-
require '
|
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
|
111
|
-
require 'active_matrix/
|
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.
|
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.
|
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.
|
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:
|
266
|
-
|
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
|
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
|