activematrix 0.0.1 → 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 (40) 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/active_matrix/bot/multi_instance_base.rb +189 -0
  6. data/lib/active_matrix/client.rb +5 -15
  7. data/lib/active_matrix/client_pool.rb +194 -0
  8. data/lib/active_matrix/event_router.rb +215 -0
  9. data/lib/active_matrix/memory/agent_memory.rb +128 -0
  10. data/lib/active_matrix/memory/base.rb +101 -0
  11. data/lib/active_matrix/memory/conversation_memory.rb +161 -0
  12. data/lib/active_matrix/memory/global_memory.rb +153 -0
  13. data/lib/active_matrix/memory.rb +28 -0
  14. data/lib/active_matrix/room.rb +131 -51
  15. data/lib/active_matrix/rooms/space.rb +1 -5
  16. data/lib/active_matrix/user.rb +10 -0
  17. data/lib/active_matrix/util/account_data_cache.rb +62 -24
  18. data/lib/active_matrix/util/cacheable.rb +73 -0
  19. data/lib/active_matrix/util/extensions.rb +4 -0
  20. data/lib/active_matrix/util/state_event_cache.rb +106 -31
  21. data/lib/active_matrix/version.rb +1 -1
  22. data/lib/active_matrix.rb +51 -3
  23. data/lib/generators/active_matrix/bot/bot_generator.rb +38 -0
  24. data/lib/generators/active_matrix/bot/templates/bot.rb.erb +111 -0
  25. data/lib/generators/active_matrix/bot/templates/bot_spec.rb.erb +68 -0
  26. data/lib/generators/active_matrix/install/install_generator.rb +44 -0
  27. data/lib/generators/active_matrix/install/templates/README +30 -0
  28. data/lib/generators/active_matrix/install/templates/active_matrix.rb +33 -0
  29. data/lib/generators/active_matrix/install/templates/agent_memory.rb +47 -0
  30. data/lib/generators/active_matrix/install/templates/conversation_context.rb +72 -0
  31. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +17 -0
  32. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +21 -0
  33. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +20 -0
  34. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +26 -0
  35. data/lib/generators/active_matrix/install/templates/global_memory.rb +70 -0
  36. data/lib/generators/active_matrix/install/templates/matrix_agent.rb +127 -0
  37. metadata +110 -4
  38. data/lib/active_matrix/util/rails_cache_adapter.rb +0 -37
  39. data/lib/active_matrix/util/tinycache.rb +0 -145
  40. data/lib/active_matrix/util/tinycache_adapter.rb +0 -87
@@ -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
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.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: simplecov
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,34 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.19'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.19'
97
139
  - !ruby/object:Gem::Dependency
98
140
  name: activerecord
99
141
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +150,34 @@ dependencies:
108
150
  - - "~>"
109
151
  - !ruby/object:Gem::Version
110
152
  version: '8.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: bcrypt
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.1'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: concurrent-ruby
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.2'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.2'
111
181
  - !ruby/object:Gem::Dependency
112
182
  name: railties
113
183
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +192,20 @@ dependencies:
122
192
  - - "~>"
123
193
  - !ruby/object:Gem::Version
124
194
  version: '8.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: state_machines-activerecord
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: 0.40.0
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: 0.40.0
125
209
  - !ruby/object:Gem::Dependency
126
210
  name: zeitwerk
127
211
  requirement: !ruby/object:Gem::Requirement
@@ -152,13 +236,23 @@ files:
152
236
  - LICENSE.txt
153
237
  - README.md
154
238
  - lib/active_matrix.rb
239
+ - lib/active_matrix/agent_manager.rb
240
+ - lib/active_matrix/agent_registry.rb
155
241
  - lib/active_matrix/api.rb
156
242
  - lib/active_matrix/bot.rb
157
243
  - lib/active_matrix/bot/base.rb
158
244
  - lib/active_matrix/bot/main.rb
245
+ - lib/active_matrix/bot/multi_instance_base.rb
159
246
  - lib/active_matrix/client.rb
247
+ - lib/active_matrix/client_pool.rb
160
248
  - lib/active_matrix/errors.rb
249
+ - lib/active_matrix/event_router.rb
161
250
  - lib/active_matrix/logging.rb
251
+ - lib/active_matrix/memory.rb
252
+ - lib/active_matrix/memory/agent_memory.rb
253
+ - lib/active_matrix/memory/base.rb
254
+ - lib/active_matrix/memory/conversation_memory.rb
255
+ - lib/active_matrix/memory/global_memory.rb
162
256
  - lib/active_matrix/mxid.rb
163
257
  - lib/active_matrix/protocols/as.rb
164
258
  - lib/active_matrix/protocols/cs.rb
@@ -171,14 +265,26 @@ files:
171
265
  - lib/active_matrix/rooms/space.rb
172
266
  - lib/active_matrix/user.rb
173
267
  - lib/active_matrix/util/account_data_cache.rb
268
+ - lib/active_matrix/util/cacheable.rb
174
269
  - lib/active_matrix/util/events.rb
175
270
  - lib/active_matrix/util/extensions.rb
176
- - lib/active_matrix/util/rails_cache_adapter.rb
177
271
  - lib/active_matrix/util/state_event_cache.rb
178
- - lib/active_matrix/util/tinycache.rb
179
- - lib/active_matrix/util/tinycache_adapter.rb
180
272
  - lib/active_matrix/util/uri.rb
181
273
  - lib/active_matrix/version.rb
274
+ - lib/generators/active_matrix/bot/bot_generator.rb
275
+ - lib/generators/active_matrix/bot/templates/bot.rb.erb
276
+ - lib/generators/active_matrix/bot/templates/bot_spec.rb.erb
277
+ - lib/generators/active_matrix/install/install_generator.rb
278
+ - lib/generators/active_matrix/install/templates/README
279
+ - lib/generators/active_matrix/install/templates/active_matrix.rb
280
+ - lib/generators/active_matrix/install/templates/agent_memory.rb
281
+ - lib/generators/active_matrix/install/templates/conversation_context.rb
282
+ - lib/generators/active_matrix/install/templates/create_agent_memories.rb
283
+ - lib/generators/active_matrix/install/templates/create_conversation_contexts.rb
284
+ - lib/generators/active_matrix/install/templates/create_global_memories.rb
285
+ - lib/generators/active_matrix/install/templates/create_matrix_agents.rb
286
+ - lib/generators/active_matrix/install/templates/global_memory.rb
287
+ - lib/generators/active_matrix/install/templates/matrix_agent.rb
182
288
  homepage: https://github.com/seuros/activematrix
183
289
  licenses:
184
290
  - MIT
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveMatrix
4
- module Util
5
- class RailsCacheAdapter
6
- attr_accessor :client
7
-
8
- def initialize
9
- @cache = ::Rails.cache
10
- end
11
-
12
- def read(key, _options = {})
13
- @cache.read(key)
14
- end
15
-
16
- def write(key, value, expires_in: nil)
17
- @cache.write(key, value, expires_in: expires_in)
18
- end
19
-
20
- def exist?(key)
21
- @cache.exist?(key)
22
- end
23
-
24
- def delete(key)
25
- @cache.delete(key)
26
- end
27
-
28
- def clear
29
- @cache.clear
30
- end
31
-
32
- def cleanup
33
- # Rails.cache handles its own cleanup
34
- end
35
- end
36
- end
37
- end