activematrix 0.0.0 → 0.0.2

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +218 -51
  3. data/lib/active_matrix/agent_manager.rb +275 -0
  4. data/lib/active_matrix/agent_registry.rb +154 -0
  5. data/lib/{matrix_sdk → active_matrix}/api.rb +18 -22
  6. data/lib/{matrix_sdk → active_matrix}/bot/base.rb +42 -39
  7. data/lib/{matrix_sdk → active_matrix}/bot/main.rb +4 -5
  8. data/lib/active_matrix/bot/multi_instance_base.rb +189 -0
  9. data/lib/active_matrix/bot.rb +7 -0
  10. data/lib/{matrix_sdk → active_matrix}/client.rb +21 -34
  11. data/lib/active_matrix/client_pool.rb +194 -0
  12. data/lib/{matrix_sdk → active_matrix}/errors.rb +4 -4
  13. data/lib/active_matrix/event_router.rb +215 -0
  14. data/lib/active_matrix/logging.rb +56 -0
  15. data/lib/active_matrix/memory/agent_memory.rb +128 -0
  16. data/lib/active_matrix/memory/base.rb +101 -0
  17. data/lib/active_matrix/memory/conversation_memory.rb +161 -0
  18. data/lib/active_matrix/memory/global_memory.rb +153 -0
  19. data/lib/active_matrix/memory.rb +28 -0
  20. data/lib/{matrix_sdk → active_matrix}/mxid.rb +2 -2
  21. data/lib/{matrix_sdk → active_matrix}/protocols/as.rb +1 -1
  22. data/lib/{matrix_sdk → active_matrix}/protocols/cs.rb +6 -8
  23. data/lib/{matrix_sdk → active_matrix}/protocols/is.rb +1 -1
  24. data/lib/{matrix_sdk → active_matrix}/protocols/msc.rb +6 -8
  25. data/lib/{matrix_sdk → active_matrix}/protocols/ss.rb +2 -2
  26. data/lib/active_matrix/railtie.rb +18 -0
  27. data/lib/{matrix_sdk → active_matrix}/response.rb +2 -2
  28. data/lib/{matrix_sdk → active_matrix}/room.rb +148 -72
  29. data/lib/{matrix_sdk → active_matrix}/rooms/space.rb +3 -7
  30. data/lib/{matrix_sdk → active_matrix}/user.rb +23 -15
  31. data/lib/active_matrix/util/account_data_cache.rb +129 -0
  32. data/lib/active_matrix/util/cacheable.rb +73 -0
  33. data/lib/{matrix_sdk → active_matrix}/util/events.rb +8 -8
  34. data/lib/{matrix_sdk → active_matrix}/util/extensions.rb +6 -15
  35. data/lib/active_matrix/util/state_event_cache.rb +167 -0
  36. data/lib/{matrix_sdk → active_matrix}/util/uri.rb +4 -4
  37. data/lib/active_matrix/version.rb +5 -0
  38. data/lib/active_matrix.rb +81 -0
  39. data/lib/generators/active_matrix/bot/bot_generator.rb +38 -0
  40. data/lib/generators/active_matrix/bot/templates/bot.rb.erb +111 -0
  41. data/lib/generators/active_matrix/bot/templates/bot_spec.rb.erb +68 -0
  42. data/lib/generators/active_matrix/install/install_generator.rb +44 -0
  43. data/lib/generators/active_matrix/install/templates/README +30 -0
  44. data/lib/generators/active_matrix/install/templates/active_matrix.rb +33 -0
  45. data/lib/generators/active_matrix/install/templates/agent_memory.rb +47 -0
  46. data/lib/generators/active_matrix/install/templates/conversation_context.rb +72 -0
  47. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +17 -0
  48. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +21 -0
  49. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +20 -0
  50. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +26 -0
  51. data/lib/generators/active_matrix/install/templates/global_memory.rb +70 -0
  52. data/lib/generators/active_matrix/install/templates/matrix_agent.rb +127 -0
  53. metadata +168 -30
  54. data/lib/matrix_sdk/bot.rb +0 -4
  55. data/lib/matrix_sdk/util/account_data_cache.rb +0 -91
  56. data/lib/matrix_sdk/util/state_event_cache.rb +0 -92
  57. data/lib/matrix_sdk/util/tinycache.rb +0 -140
  58. data/lib/matrix_sdk/util/tinycache_adapter.rb +0 -87
  59. data/lib/matrix_sdk/version.rb +0 -5
  60. data/lib/matrix_sdk.rb +0 -75
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module ActiveMatrix
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ desc 'Creates ActiveMatrix migrations and initializers'
14
+
15
+ def create_migrations
16
+ migration_template 'create_matrix_agents.rb', 'db/migrate/create_matrix_agents.rb'
17
+ migration_template 'create_agent_memories.rb', 'db/migrate/create_agent_memories.rb'
18
+ migration_template 'create_conversation_contexts.rb', 'db/migrate/create_conversation_contexts.rb'
19
+ migration_template 'create_global_memories.rb', 'db/migrate/create_global_memories.rb'
20
+ end
21
+
22
+ def create_initializer
23
+ template 'active_matrix.rb', 'config/initializers/active_matrix.rb'
24
+ end
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
+ def display_post_install
34
+ readme 'README' if behavior == :invoke
35
+ end
36
+
37
+ private
38
+
39
+ def migration_version
40
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ ===============================================================================
2
+
3
+ ActiveMatrix Multi-Agent System has been installed!
4
+
5
+ Next steps:
6
+
7
+ 1. Run migrations:
8
+ rails db:migrate
9
+
10
+ 2. Create your first bot:
11
+ rails generate active_matrix:bot MyFirstBot
12
+
13
+ 3. Configure your agents in the Rails console:
14
+ agent = MatrixAgent.create!(
15
+ name: 'captain',
16
+ homeserver: 'https://matrix.org',
17
+ username: 'captain_bot',
18
+ password: 'secure_password',
19
+ bot_class: 'MyFirstBot'
20
+ )
21
+
22
+ 4. Start the agent system:
23
+ ActiveMatrix::AgentManager.instance.start_all
24
+
25
+ Or start individual agents:
26
+ ActiveMatrix::AgentManager.instance.start_agent(agent)
27
+
28
+ For more information, visit: https://github.com/seuros/agent_smith
29
+
30
+ ===============================================================================
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveMatrix configuration
4
+ ActiveMatrix.configure do |config|
5
+ # Agent configuration
6
+ config.agent_startup_delay = 2.seconds # Delay between starting each agent
7
+ config.max_agents_per_process = 10 # Maximum agents in a single process
8
+ config.agent_health_check_interval = 30.seconds
9
+
10
+ # Memory configuration
11
+ config.conversation_history_limit = 20
12
+ config.conversation_stale_after = 1.day
13
+ config.memory_cleanup_interval = 1.hour
14
+
15
+ # Event routing configuration
16
+ config.event_queue_size = 1000
17
+ config.event_processing_timeout = 30.seconds
18
+
19
+ # Client pool configuration
20
+ config.max_clients_per_homeserver = 5
21
+ config.client_idle_timeout = 5.minutes
22
+
23
+ # Logging
24
+ config.agent_log_level = :info
25
+ config.log_agent_events = Rails.env.development?
26
+ end
27
+
28
+ # Start agent manager on Rails boot (can be disabled in environments)
29
+ if Rails.env.production? || ENV['START_AGENTS'].present?
30
+ Rails.application.config.after_initialize do
31
+ ActiveMatrix::AgentManager.instance.start_all
32
+ end
33
+ end
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,72 @@
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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAgentMemories < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :agent_memories do |t|
6
+ t.references :matrix_agent, null: false, foreign_key: true
7
+ t.string :key, null: false
8
+ t.jsonb :value, default: {}
9
+ t.datetime :expires_at
10
+
11
+ t.timestamps
12
+
13
+ t.index [:matrix_agent_id, :key], unique: true
14
+ t.index :expires_at
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateConversationContexts < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :conversation_contexts do |t|
6
+ t.references :matrix_agent, null: false, foreign_key: true
7
+ t.string :user_id, null: false
8
+ t.string :room_id, null: false
9
+ t.jsonb :context, default: {}
10
+ t.jsonb :message_history, default: { messages: [] }
11
+ t.datetime :last_message_at
12
+ t.integer :message_count, default: 0
13
+
14
+ t.timestamps
15
+
16
+ t.index [:matrix_agent_id, :user_id, :room_id], unique: true, name: 'idx_conv_context_unique'
17
+ t.index :last_message_at
18
+ t.index :room_id
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGlobalMemories < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :global_memories do |t|
6
+ t.string :key, null: false
7
+ t.jsonb :value, default: {}
8
+ t.string :category
9
+ t.datetime :expires_at
10
+ t.boolean :public_read, default: true
11
+ t.boolean :public_write, default: false
12
+
13
+ t.timestamps
14
+
15
+ t.index :key, unique: true
16
+ t.index :category
17
+ t.index :expires_at
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateMatrixAgents < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :matrix_agents do |t|
6
+ t.string :name, null: false
7
+ t.string :homeserver, null: false
8
+ t.string :username, null: false
9
+ t.string :encrypted_password
10
+ t.string :access_token
11
+ t.string :state, default: 'offline', null: false
12
+ t.string :bot_class, null: false
13
+ t.jsonb :settings, default: {}
14
+ t.string :last_sync_token
15
+ t.datetime :last_active_at
16
+ t.integer :rooms_count, default: 0
17
+ t.integer :messages_handled, default: 0
18
+
19
+ t.timestamps
20
+
21
+ t.index :name, unique: true
22
+ t.index :state
23
+ t.index [:homeserver, :username]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
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
@@ -0,0 +1,127 @@
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