activematrix 0.0.5 → 0.0.8

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -28
  3. data/app/jobs/active_matrix/application_job.rb +11 -0
  4. data/app/models/active_matrix/agent/jobs/memory_reaper.rb +87 -0
  5. data/app/models/active_matrix/agent.rb +166 -0
  6. data/app/models/active_matrix/agent_store.rb +80 -0
  7. data/app/models/active_matrix/application_record.rb +15 -0
  8. data/app/models/active_matrix/chat_session.rb +105 -0
  9. data/app/models/active_matrix/knowledge_base.rb +100 -0
  10. data/exe/activematrix +7 -0
  11. data/lib/active_matrix/agent_manager.rb +160 -121
  12. data/lib/active_matrix/agent_registry.rb +25 -21
  13. data/lib/active_matrix/api.rb +8 -2
  14. data/lib/active_matrix/async_query.rb +58 -0
  15. data/lib/active_matrix/bot/base.rb +3 -3
  16. data/lib/active_matrix/bot/builtin_commands.rb +188 -0
  17. data/lib/active_matrix/bot/command_parser.rb +175 -0
  18. data/lib/active_matrix/cli.rb +273 -0
  19. data/lib/active_matrix/client.rb +21 -6
  20. data/lib/active_matrix/client_pool.rb +38 -27
  21. data/lib/active_matrix/daemon/probe_server.rb +118 -0
  22. data/lib/active_matrix/daemon/signal_handler.rb +156 -0
  23. data/lib/active_matrix/daemon/worker.rb +109 -0
  24. data/lib/active_matrix/daemon.rb +236 -0
  25. data/lib/active_matrix/engine.rb +18 -0
  26. data/lib/active_matrix/errors.rb +1 -1
  27. data/lib/active_matrix/event_router.rb +61 -49
  28. data/lib/active_matrix/events.rb +1 -0
  29. data/lib/active_matrix/instrumentation.rb +148 -0
  30. data/lib/active_matrix/memory/agent_memory.rb +7 -21
  31. data/lib/active_matrix/memory/conversation_memory.rb +4 -20
  32. data/lib/active_matrix/memory/global_memory.rb +15 -30
  33. data/lib/active_matrix/message_dispatcher.rb +197 -0
  34. data/lib/active_matrix/metrics.rb +424 -0
  35. data/lib/active_matrix/presence_manager.rb +181 -0
  36. data/lib/active_matrix/railtie.rb +8 -0
  37. data/lib/active_matrix/telemetry.rb +134 -0
  38. data/lib/active_matrix/version.rb +1 -1
  39. data/lib/active_matrix.rb +18 -11
  40. data/lib/generators/active_matrix/install/install_generator.rb +3 -22
  41. data/lib/generators/active_matrix/install/templates/README +5 -2
  42. metadata +191 -31
  43. data/lib/generators/active_matrix/install/templates/agent_memory.rb +0 -47
  44. data/lib/generators/active_matrix/install/templates/conversation_context.rb +0 -72
  45. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
  46. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
  47. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
  48. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
  49. data/lib/generators/active_matrix/install/templates/global_memory.rb +0 -70
  50. 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: 8fd68a9263d0fb04aaa3f2d0d3160b26b995e783ebc12fa0cf9d67c0084e3d5b
4
- data.tar.gz: b282050de6c58f9c470a19f1f17c44f1d6cba0e60b0ee0907e9dd1f82d1395e7
3
+ metadata.gz: 91157233aaf3c81b35092f75d78eb3ebab06a9f0d93c40a9b098283cec571add
4
+ data.tar.gz: f9b0de48655ee41174628f26d6683caca5b35aa4e3e54664d79ae859086fa670
5
5
  SHA512:
6
- metadata.gz: 6f90166bf9f0e25db87953d2a79bfec7ddf04542ff975ddf15602f00fbcad5b0f909be3652b85c97ee771e3aab8aca79bb2e01226d35a59c79ba1ae338f2a722
7
- data.tar.gz: 969d6efbe9f3a65294b983410c3737939dca5cb281fa52f55baaa8de317151126e9cc245b1a693398819053bad8e40c7391157f00a55f3835b98d178cd4fa0e7
6
+ metadata.gz: 13a5cf30e83f2b665cd52fd110b4d2bf1ae4bb95a41586e8fa8c670c0cc2370c8bfbff16d835aacf4d8b484ea2cd59096fc542b5bc5643d060dbf307c2f00502
7
+ data.tar.gz: bc74041cc3d86999d9113a6c4e6a273aeef54f6ba3682981b180b5c3006ab7c6bd85f4049461cb908253ae1471a78f62245bddd8f3b099be715548c4761ebc7a
data/README.md CHANGED
@@ -2,23 +2,31 @@
2
2
 
3
3
  A Rails-native Matrix SDK for building multi-agent bot systems and real-time communication features. This gem is a fork of the [matrix-sdk](https://github.com/ananace/ruby-matrix-sdk) gem, extensively enhanced with Rails integration, multi-agent architecture, and persistent state management.
4
4
 
5
+ ## Requirements
6
+
7
+ - **Ruby 3.4+**
8
+ - **Rails 8.0+**
9
+ - **PostgreSQL 18+** (required for UUIDv7 primary keys)
10
+
5
11
  ## Features
6
12
 
7
- - **Multi-Agent Architecture**: Run multiple bots concurrently with lifecycle management
13
+ - **Multi-Agent Architecture**: Run multiple bots concurrently with async fiber-based lifecycle management
14
+ - **Daemon Binary**: Production-ready `activematrix` daemon with multi-process workers and health probes
8
15
  - **Rails Integration**: Deep integration with ActiveRecord, Rails.cache, and Rails.logger
9
16
  - **State Machines**: state_machines-powered state management for bot lifecycle
10
17
  - **Memory System**: Three-tier memory architecture (agent, conversation, global)
11
18
  - **Event Routing**: Intelligent event distribution to appropriate agents
12
- - **Client Pooling**: Efficient connection management for multiple bots
19
+ - **Client Pooling**: Efficient connection management with async semaphores
13
20
  - **Generators**: Rails generators for quick bot creation
14
21
  - **Inter-Agent Communication**: Built-in messaging between bots
22
+ - **PostgreSQL 18 Features**: UUIDv7 primary keys, JSONB with GIN indexes
15
23
 
16
24
  ## Installation
17
25
 
18
26
  Add this line to your application's Gemfile:
19
27
 
20
28
  ```ruby
21
- gem 'activematrix', '~> 0.0.3'
29
+ gem 'activematrix'
22
30
  ```
23
31
 
24
32
  And then execute:
@@ -43,7 +51,7 @@ This creates a bot class in `app/bots/captain_bot.rb`:
43
51
  class CaptainBot < ActiveMatrix::Bot::MultiInstanceBase
44
52
  set :accept_invites, true
45
53
  set :command_prefix, '!'
46
-
54
+
47
55
  command :status,
48
56
  desc: 'Get system status',
49
57
  args: '[component]' do |component = nil|
@@ -56,7 +64,7 @@ class CaptainBot < ActiveMatrix::Bot::MultiInstanceBase
56
64
  room.send_notice("All systems operational!")
57
65
  end
58
66
  end
59
-
67
+
60
68
  command :deploy,
61
69
  desc: 'Deploy to production',
62
70
  args: 'target' do |target|
@@ -64,17 +72,17 @@ class CaptainBot < ActiveMatrix::Bot::MultiInstanceBase
64
72
  deployments = conversation_memory.remember(:deployments) { [] }
65
73
  deployments << { target: target, time: Time.current }
66
74
  conversation_memory[:deployments] = deployments
67
-
75
+
68
76
  # Notify other agents
69
77
  broadcast_to_agents(:lieutenant, {
70
78
  type: 'deployment',
71
79
  target: target,
72
80
  initiated_by: agent_name
73
81
  })
74
-
82
+
75
83
  room.send_notice("Deploying to #{target}...")
76
84
  end
77
-
85
+
78
86
  # Handle inter-agent messages
79
87
  def receive_message(data, from:)
80
88
  case data[:type]
@@ -90,7 +98,7 @@ end
90
98
 
91
99
  ```ruby
92
100
  # Create agent records in Rails console or seeds
93
- captain = MatrixAgent.create!(
101
+ captain = ActiveMatrix::Agent.create!(
94
102
  name: 'captain',
95
103
  homeserver: 'https://matrix.org',
96
104
  username: 'captain_bot',
@@ -102,8 +110,8 @@ captain = MatrixAgent.create!(
102
110
  }
103
111
  )
104
112
 
105
- lieutenant = MatrixAgent.create!(
106
- name: 'lieutenant',
113
+ lieutenant = ActiveMatrix::Agent.create!(
114
+ name: 'lieutenant',
107
115
  homeserver: 'https://matrix.org',
108
116
  username: 'lieutenant_bot',
109
117
  password: 'secure_password',
@@ -111,10 +119,47 @@ lieutenant = MatrixAgent.create!(
111
119
  )
112
120
  ```
113
121
 
114
- ### Managing Agents
122
+ ### Running the Daemon
123
+
124
+ The `activematrix` binary manages your bots in production, similar to Sidekiq or GoodJob:
125
+
126
+ ```bash
127
+ # Start in foreground
128
+ bundle exec activematrix start
129
+
130
+ # Start with multiple worker processes
131
+ bundle exec activematrix start --workers 3
132
+
133
+ # Start specific agents only
134
+ bundle exec activematrix start --agents captain,lieutenant
135
+
136
+ # Daemonize with PID file
137
+ bundle exec activematrix start --daemon --pidfile tmp/pids/activematrix.pid
138
+
139
+ # Check status (queries health probe)
140
+ bundle exec activematrix status
141
+
142
+ # Graceful shutdown
143
+ bundle exec activematrix stop
144
+
145
+ # Reload agent configuration
146
+ bundle exec activematrix reload
147
+ ```
148
+
149
+ **Health Probes** (for Kubernetes/Docker):
150
+ - `GET /health` - Returns 200 if healthy
151
+ - `GET /status` - JSON with detailed agent status
152
+ - `GET /metrics` - Prometheus-compatible metrics
153
+
154
+ ```bash
155
+ curl http://localhost:3042/health
156
+ curl http://localhost:3042/status
157
+ ```
158
+
159
+ ### Programmatic Agent Management
115
160
 
116
161
  ```ruby
117
- # Start all agents
162
+ # Start all agents (blocks until shutdown)
118
163
  ActiveMatrix::AgentManager.instance.start_all
119
164
 
120
165
  # Start specific agent
@@ -156,7 +201,7 @@ recent = conversation_memory.recent_messages(5)
156
201
  ### Global Memory (Shared)
157
202
  ```ruby
158
203
  # Set global data
159
- global_memory.set('system_status', 'operational',
204
+ global_memory.set('system_status', 'operational',
160
205
  category: 'monitoring',
161
206
  expires_in: 5.minutes,
162
207
  public_read: true
@@ -180,7 +225,7 @@ class MonitorBot < ActiveMatrix::Bot::MultiInstanceBase
180
225
  route event_type: 'm.room.message', priority: 100 do |bot, event|
181
226
  # Custom processing
182
227
  end
183
-
228
+
184
229
  route room_id: '!monitoring:matrix.org' do |bot, event|
185
230
  # Handle all events from monitoring room
186
231
  end
@@ -205,26 +250,40 @@ room.send_text "Hello from ActiveMatrix!"
205
250
  ```ruby
206
251
  # config/initializers/active_matrix.rb
207
252
  ActiveMatrix.configure do |config|
253
+ # Agent settings
208
254
  config.agent_startup_delay = 2.seconds
209
255
  config.max_agents_per_process = 10
210
256
  config.agent_health_check_interval = 30.seconds
257
+
258
+ # Memory settings
211
259
  config.conversation_history_limit = 20
212
260
  config.conversation_stale_after = 1.day
213
261
  config.memory_cleanup_interval = 1.hour
262
+
263
+ # Daemon settings
264
+ config.daemon_workers = 2
265
+ config.probe_port = 3042
266
+ config.probe_host = '0.0.0.0'
267
+ config.shutdown_timeout = 30
214
268
  end
215
269
  ```
216
270
 
217
271
  ## Testing
218
272
 
219
273
  ```ruby
220
- # spec/bots/captain_bot_spec.rb
221
- RSpec.describe CaptainBot do
222
- let(:agent) { create(:matrix_agent, bot_class: 'CaptainBot') }
223
- let(:bot) { described_class.new(agent) }
224
-
225
- it 'responds to status command' do
226
- expect(room).to receive(:send_notice).with(/operational/)
227
- bot.status
274
+ # test/bots/captain_bot_test.rb
275
+ class CaptainBotTest < ActiveSupport::TestCase
276
+ def setup
277
+ @agent = ActiveMatrix::Agent.create!(
278
+ name: 'test_captain',
279
+ homeserver: 'https://matrix.org',
280
+ username: 'test_bot',
281
+ bot_class: 'CaptainBot'
282
+ )
283
+ end
284
+
285
+ test 'responds to status command' do
286
+ # Your test logic here
228
287
  end
229
288
  end
230
289
  ```
@@ -233,16 +292,25 @@ end
233
292
 
234
293
  ActiveMatrix implements a sophisticated multi-agent architecture:
235
294
 
236
- - **AgentManager**: Manages lifecycle of all bots (start/stop/restart)
237
- - **AgentRegistry**: Thread-safe registry of running bot instances
238
- - **EventRouter**: Routes Matrix events to appropriate bots
239
- - **ClientPool**: Manages shared client connections efficiently
295
+ - **AgentManager**: Manages lifecycle of all bots using async fibers (start/stop/restart)
296
+ - **AgentRegistry**: Fiber-safe registry of running bot instances
297
+ - **EventRouter**: Routes Matrix events to appropriate bots via async queues
298
+ - **ClientPool**: Manages shared client connections with async semaphores
240
299
  - **Memory System**: Hierarchical storage with caching
241
300
  - **State Machines**: Track agent states (offline/connecting/online/busy/error)
242
301
 
302
+ ### Models
303
+
304
+ All models are namespaced under `ActiveMatrix::`:
305
+
306
+ - `ActiveMatrix::Agent` - Bot agent records with state machine
307
+ - `ActiveMatrix::AgentStore` - Per-agent key-value storage
308
+ - `ActiveMatrix::ChatSession` - Conversation context per user/room
309
+ - `ActiveMatrix::KnowledgeBase` - Global shared storage
310
+
243
311
  ## Contributing
244
312
 
245
- Bug reports and pull requests are welcome on GitHub at https://github.com/seuros/agent_smith
313
+ Bug reports and pull requests are welcome on GitHub at https://github.com/seuros/activematrix
246
314
 
247
315
  ## License
248
316
 
@@ -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,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bcrypt'
4
+
5
+ # <rails-lens:schema:begin>
6
+ # table = "active_matrix_agents"
7
+ # database_dialect = "PostgreSQL"
8
+ #
9
+ # columns = [
10
+ # { name = "id", type = "integer", pk = true, null = false },
11
+ # { name = "name", type = "string", null = false },
12
+ # { name = "homeserver", type = "string", null = false },
13
+ # { name = "username", type = "string", null = false },
14
+ # { name = "bot_class", type = "string", null = false },
15
+ # { name = "state", type = "string", null = false, default = "offline" },
16
+ # { name = "access_token", type = "string" },
17
+ # { name = "encrypted_password", type = "string" },
18
+ # { name = "settings", type = "json" },
19
+ # { name = "last_sync_token", type = "string" },
20
+ # { name = "last_active_at", type = "datetime" },
21
+ # { name = "messages_handled", type = "integer", null = false, default = "0" },
22
+ # { name = "created_at", type = "datetime", null = false },
23
+ # { name = "updated_at", type = "datetime", null = false }
24
+ # ]
25
+ #
26
+ # indexes = [
27
+ # { name = "index_active_matrix_agents_on_homeserver", columns = ["homeserver"] },
28
+ # { name = "index_active_matrix_agents_on_name", columns = ["name"], unique = true },
29
+ # { name = "index_active_matrix_agents_on_state", columns = ["state"] }
30
+ # ]
31
+ #
32
+ # [callbacks]
33
+ # before_save = [{ method = "encrypt_password", if = ["password_changed?"] }]
34
+ # around_validation = [{ method = "machine" }]
35
+ #
36
+ # notes = ["agent_stores:INVERSE_OF", "chat_sessions:INVERSE_OF", "agent_stores:N_PLUS_ONE", "chat_sessions:N_PLUS_ONE", "access_token:NOT_NULL", "encrypted_password:NOT_NULL", "settings:NOT_NULL", "name:LIMIT", "homeserver:LIMIT", "username:LIMIT", "bot_class:LIMIT", "state:LIMIT", "access_token:LIMIT", "encrypted_password:LIMIT", "last_sync_token:LIMIT", "username:INDEX", "access_token:INDEX", "last_sync_token:INDEX"]
37
+ # <rails-lens:schema:end>
38
+ module ActiveMatrix
39
+ class Agent < ApplicationRecord
40
+ self.table_name = 'active_matrix_agents'
41
+
42
+ # Associations
43
+ has_many :agent_stores, class_name: 'ActiveMatrix::AgentStore', dependent: :destroy
44
+ has_many :chat_sessions, class_name: 'ActiveMatrix::ChatSession', dependent: :destroy
45
+
46
+ # Validations
47
+ validates :name, presence: true, uniqueness: true
48
+ validates :homeserver, presence: true
49
+ validates :username, presence: true
50
+ validates :bot_class, presence: true
51
+ validate :valid_bot_class?
52
+
53
+ # Scopes
54
+ scope :active, -> { where.not(state: %i[offline error]) }
55
+ scope :online, -> { where(state: %i[online_idle online_busy]) }
56
+ scope :offline, -> { where(state: :offline) }
57
+
58
+ # Encrypts password before saving
59
+ before_save :encrypt_password, if: :password_changed?
60
+
61
+ # State machine for agent lifecycle
62
+ state_machine :state, initial: :offline do
63
+ state :offline
64
+ state :connecting
65
+ state :online_idle
66
+ state :online_busy
67
+ state :error
68
+ state :paused
69
+
70
+ event :connect do
71
+ transition %i[offline error paused] => :connecting
72
+ end
73
+
74
+ event :connection_established do
75
+ transition connecting: :online_idle
76
+ end
77
+
78
+ after_transition to: :online_idle do |agent|
79
+ agent.update_column(:last_active_at, Time.current)
80
+ end
81
+
82
+ event :start_processing do
83
+ transition online_idle: :online_busy
84
+ end
85
+
86
+ event :finish_processing do
87
+ transition online_busy: :online_idle
88
+ end
89
+
90
+ event :disconnect do
91
+ transition %i[connecting online_idle online_busy] => :offline
92
+ end
93
+
94
+ event :encounter_error do
95
+ transition any => :error
96
+ end
97
+
98
+ event :pause do
99
+ transition %i[online_idle online_busy] => :paused
100
+ end
101
+
102
+ event :resume do
103
+ transition paused: :connecting
104
+ end
105
+ end
106
+
107
+ # Instance methods
108
+ def bot_instance
109
+ @bot_instance ||= bot_class.constantize.new(client) if running?
110
+ end
111
+
112
+ def client
113
+ @client ||= if access_token.present?
114
+ ActiveMatrix::Client.new(homeserver, access_token: access_token)
115
+ else
116
+ ActiveMatrix::Client.new(homeserver)
117
+ end
118
+ end
119
+
120
+ def running?
121
+ %i[online_idle online_busy].include?(state.to_sym)
122
+ end
123
+
124
+ def memory
125
+ @memory ||= ActiveMatrix::Memory::AgentMemory.new(self)
126
+ end
127
+
128
+ def increment_messages_handled!
129
+ update!(messages_handled: messages_handled + 1)
130
+ end
131
+
132
+ def update_activity!
133
+ update(last_active_at: Time.current)
134
+ end
135
+
136
+ # Password handling
137
+ attr_accessor :password
138
+
139
+ def authenticate(password)
140
+ return false if encrypted_password.blank?
141
+
142
+ BCrypt::Password.new(encrypted_password) == password
143
+ end
144
+
145
+ private
146
+
147
+ def password_changed?
148
+ password.present?
149
+ end
150
+
151
+ def encrypt_password
152
+ self.encrypted_password = BCrypt::Password.create(password) if password.present?
153
+ end
154
+
155
+ def valid_bot_class?
156
+ return false if bot_class.blank?
157
+
158
+ begin
159
+ klass = bot_class.constantize
160
+ errors.add(:bot_class, 'must inherit from ActiveMatrix::Bot::Base') unless klass < ActiveMatrix::Bot::Base
161
+ rescue NameError
162
+ errors.add(:bot_class, 'must be a valid class name')
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <rails-lens:schema:begin>
4
+ # table = "active_matrix_agent_stores"
5
+ # database_dialect = "PostgreSQL"
6
+ #
7
+ # columns = [
8
+ # { name = "id", type = "integer", pk = true, null = false },
9
+ # { name = "agent_id", type = "integer", null = false },
10
+ # { name = "key", type = "string", null = false },
11
+ # { name = "value", type = "json" },
12
+ # { name = "expires_at", type = "datetime" },
13
+ # { name = "created_at", type = "datetime", null = false },
14
+ # { name = "updated_at", type = "datetime", null = false }
15
+ # ]
16
+ #
17
+ # indexes = [
18
+ # { name = "index_active_matrix_agent_stores_on_agent_id", columns = ["agent_id"] },
19
+ # { name = "index_active_matrix_agent_stores_on_agent_id_and_key", columns = ["agent_id", "key"], unique = true },
20
+ # { name = "index_active_matrix_agent_stores_on_expires_at", columns = ["expires_at"] }
21
+ # ]
22
+ #
23
+ # foreign_keys = [
24
+ # { column = "agent_id", references_table = "active_matrix_agents", references_column = "id", name = "fk_rails_59b3dc556f" }
25
+ # ]
26
+ #
27
+ # [callbacks]
28
+ # after_commit = [{ method = "schedule_cleanup", if = ["expires_at?"] }]
29
+ #
30
+ # notes = ["agent:INVERSE_OF", "value:NOT_NULL", "key:LIMIT"]
31
+ # <rails-lens:schema:end>
32
+ module ActiveMatrix
33
+ class AgentStore < ApplicationRecord
34
+ self.table_name = 'active_matrix_agent_stores'
35
+
36
+ belongs_to :agent, class_name: 'ActiveMatrix::Agent'
37
+
38
+ validates :key, presence: true, uniqueness: { scope: :agent_id }
39
+
40
+ scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
41
+ scope :expired, -> { where(expires_at: ..Time.current) }
42
+
43
+ # Automatically clean up expired memories
44
+ after_commit :schedule_cleanup, if: :expires_at?
45
+
46
+ def expired?
47
+ expires_at.present? && expires_at <= Time.current
48
+ end
49
+
50
+ def ttl=(seconds)
51
+ self.expires_at = seconds.present? ? Time.current + seconds : nil
52
+ end
53
+
54
+ def ttl
55
+ return nil if expires_at.blank?
56
+
57
+ remaining = expires_at - Time.current
58
+ [remaining, 0].max
59
+ end
60
+
61
+ # Cache integration
62
+ def cache_key
63
+ "agent_memory/#{agent_id}/#{key}"
64
+ end
65
+
66
+ def write_to_cache
67
+ Rails.cache.write(cache_key, value, expires_in: ttl)
68
+ end
69
+
70
+ def self.cleanup_expired!
71
+ expired.destroy_all
72
+ end
73
+
74
+ private
75
+
76
+ def schedule_cleanup
77
+ ActiveMatrix::Agent::Jobs::MemoryReaper.set(wait_until: expires_at).perform_later if defined?(ActiveMatrix::Agent::Jobs::MemoryReaper)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # <rails-lens:schema:begin>
4
+ # connection = "primary"
5
+ # database_dialect = "SQLite"
6
+ # database_version = "3.50.4"
7
+ #
8
+ # # This is an abstract class that establishes a database connection
9
+ # # but does not have an associated table.
10
+ # <rails-lens:schema:end>
11
+ module ActiveMatrix
12
+ class ApplicationRecord < ActiveRecord::Base
13
+ self.abstract_class = true
14
+ end
15
+ end