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.
- checksums.yaml +4 -4
- data/README.md +96 -28
- 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 +166 -0
- data/app/models/active_matrix/agent_store.rb +80 -0
- data/app/models/active_matrix/application_record.rb +15 -0
- data/app/models/active_matrix/chat_session.rb +105 -0
- data/app/models/active_matrix/knowledge_base.rb +100 -0
- data/exe/activematrix +7 -0
- data/lib/active_matrix/agent_manager.rb +160 -121
- data/lib/active_matrix/agent_registry.rb +25 -21
- data/lib/active_matrix/api.rb +8 -2
- data/lib/active_matrix/async_query.rb +58 -0
- data/lib/active_matrix/bot/base.rb +3 -3
- data/lib/active_matrix/bot/builtin_commands.rb +188 -0
- data/lib/active_matrix/bot/command_parser.rb +175 -0
- data/lib/active_matrix/cli.rb +273 -0
- data/lib/active_matrix/client.rb +21 -6
- data/lib/active_matrix/client_pool.rb +38 -27
- data/lib/active_matrix/daemon/probe_server.rb +118 -0
- data/lib/active_matrix/daemon/signal_handler.rb +156 -0
- data/lib/active_matrix/daemon/worker.rb +109 -0
- data/lib/active_matrix/daemon.rb +236 -0
- data/lib/active_matrix/engine.rb +18 -0
- data/lib/active_matrix/errors.rb +1 -1
- data/lib/active_matrix/event_router.rb +61 -49
- data/lib/active_matrix/events.rb +1 -0
- data/lib/active_matrix/instrumentation.rb +148 -0
- data/lib/active_matrix/memory/agent_memory.rb +7 -21
- data/lib/active_matrix/memory/conversation_memory.rb +4 -20
- data/lib/active_matrix/memory/global_memory.rb +15 -30
- data/lib/active_matrix/message_dispatcher.rb +197 -0
- data/lib/active_matrix/metrics.rb +424 -0
- data/lib/active_matrix/presence_manager.rb +181 -0
- data/lib/active_matrix/railtie.rb +8 -0
- data/lib/active_matrix/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +18 -11
- data/lib/generators/active_matrix/install/install_generator.rb +3 -22
- data/lib/generators/active_matrix/install/templates/README +5 -2
- metadata +191 -31
- 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/create_agent_memories.rb +0 -17
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
- 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: 91157233aaf3c81b35092f75d78eb3ebab06a9f0d93c40a9b098283cec571add
|
|
4
|
+
data.tar.gz: f9b0de48655ee41174628f26d6683caca5b35aa4e3e54664d79ae859086fa670
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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'
|
|
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 =
|
|
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 =
|
|
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
|
-
###
|
|
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
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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**:
|
|
238
|
-
- **EventRouter**: Routes Matrix events to appropriate bots
|
|
239
|
-
- **ClientPool**: Manages shared client connections
|
|
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/
|
|
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
|