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,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'concurrent'
5
+
6
+ module ActiveMatrix
7
+ # Routes Matrix events to appropriate agents
8
+ class EventRouter
9
+ include Singleton
10
+ include ActiveMatrix::Logging
11
+
12
+ def initialize
13
+ @routes = Concurrent::Array.new
14
+ @event_queue = Queue.new
15
+ @processing = false
16
+ @worker_thread = nil
17
+ end
18
+
19
+ # Register an event route
20
+ def register_route(agent_id:, room_id: nil, event_type: nil, user_id: nil, priority: 50, &block)
21
+ route = {
22
+ id: SecureRandom.uuid,
23
+ agent_id: agent_id,
24
+ room_id: room_id,
25
+ event_type: event_type,
26
+ user_id: user_id,
27
+ priority: priority,
28
+ handler: block
29
+ }
30
+
31
+ @routes << route
32
+ @routes.sort_by! { |r| -r[:priority] } # Higher priority first
33
+
34
+ logger.debug "Registered route: #{route.except(:handler).inspect}"
35
+ route[:id]
36
+ end
37
+
38
+ # Unregister a route
39
+ def unregister_route(route_id)
40
+ @routes.delete_if { |route| route[:id] == route_id }
41
+ end
42
+
43
+ # Clear all routes for an agent
44
+ def clear_agent_routes(agent_id)
45
+ @routes.delete_if { |route| route[:agent_id] == agent_id }
46
+ end
47
+
48
+ # Route an event to appropriate agents
49
+ def route_event(event)
50
+ return unless @processing
51
+
52
+ # Queue the event for processing
53
+ @event_queue << event
54
+ end
55
+
56
+ # Start the event router
57
+ def start
58
+ return if @processing
59
+
60
+ @processing = true
61
+ @worker_thread = Thread.new do
62
+ Thread.current.name = 'event-router'
63
+ process_events
64
+ end
65
+
66
+ logger.info 'Event router started'
67
+ end
68
+
69
+ # Stop the event router
70
+ def stop
71
+ @processing = false
72
+ @worker_thread&.kill
73
+ @event_queue.clear
74
+
75
+ logger.info 'Event router stopped'
76
+ end
77
+
78
+ # Check if router is running
79
+ def running?
80
+ @processing && @worker_thread&.alive?
81
+ end
82
+
83
+ # Get routes for debugging
84
+ def routes_summary
85
+ @routes.map { |r| r.except(:handler) }
86
+ end
87
+
88
+ # Broadcast an event to all agents
89
+ def broadcast_event(event)
90
+ AgentRegistry.instance.all_instances.each do |bot|
91
+ bot._handle_event(event) if bot.respond_to?(:_handle_event)
92
+ rescue StandardError => e
93
+ logger.error "Error broadcasting to bot: #{e.message}"
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def process_events
100
+ while @processing
101
+ begin
102
+ # Wait for event with timeout
103
+ event = nil
104
+ Timeout.timeout(1) { event = @event_queue.pop }
105
+
106
+ next unless event
107
+
108
+ # Find matching routes
109
+ matching_routes = find_matching_routes(event)
110
+
111
+ if matching_routes.empty?
112
+ logger.debug "No routes matched for event: #{event[:type]} in #{event[:room_id]}"
113
+ next
114
+ end
115
+
116
+ # Process routes in priority order
117
+ matching_routes.each do |route|
118
+ process_route(route, event)
119
+ end
120
+ rescue Timeout::Error
121
+ # Normal timeout, continue loop
122
+ rescue StandardError => e
123
+ logger.error "Event router error: #{e.message}"
124
+ logger.error e.backtrace.join("\n")
125
+ end
126
+ end
127
+ end
128
+
129
+ def find_matching_routes(event)
130
+ @routes.select do |route|
131
+ # Check room match
132
+ next false if route[:room_id] && route[:room_id] != event[:room_id]
133
+
134
+ # Check event type match
135
+ next false if route[:event_type] && route[:event_type] != event[:type]
136
+
137
+ # Check user match
138
+ next false if route[:user_id] && route[:user_id] != event[:sender]
139
+
140
+ # Check if agent is running
141
+ registry = AgentRegistry.instance
142
+ agent_entry = registry.get(route[:agent_id])
143
+ next false unless agent_entry
144
+
145
+ true
146
+ end
147
+ end
148
+
149
+ def process_route(route, event)
150
+ registry = AgentRegistry.instance
151
+ agent_entry = registry.get(route[:agent_id])
152
+
153
+ return unless agent_entry
154
+
155
+ bot = agent_entry[:instance]
156
+
157
+ begin
158
+ if route[:handler]
159
+ # Custom handler
160
+ route[:handler].call(bot, event)
161
+ elsif bot.respond_to?(:_handle_event)
162
+ # Default handling
163
+ bot._handle_event(event)
164
+ end
165
+ rescue StandardError => e
166
+ logger.error "Error processing route for agent #{agent_entry[:record].name}: #{e.message}"
167
+ logger.error e.backtrace.first(5).join("\n")
168
+ end
169
+ end
170
+ end
171
+
172
+ # Routing DSL for bots
173
+ module Bot
174
+ class MultiInstanceBase
175
+ # Route events to this bot
176
+ def self.route(event_type: nil, room_id: nil, user_id: nil, priority: 50, &block)
177
+ # Routes will be registered when bot instance is created
178
+ @event_routes ||= []
179
+ @event_routes << {
180
+ event_type: event_type,
181
+ room_id: room_id,
182
+ user_id: user_id,
183
+ priority: priority,
184
+ handler: block
185
+ }
186
+ end
187
+
188
+ # Get defined routes
189
+ def self.event_routes
190
+ @event_routes || []
191
+ end
192
+
193
+ # Register routes for this instance
194
+ def register_routes
195
+ return unless @agent_record
196
+
197
+ router = EventRouter.instance
198
+
199
+ self.class.event_routes.each do |route_def|
200
+ router.register_route(
201
+ agent_id: @agent_record.id,
202
+ **route_def
203
+ )
204
+ end
205
+ end
206
+
207
+ # Clear routes for this instance
208
+ def clear_routes
209
+ return unless @agent_record
210
+
211
+ EventRouter.instance.clear_agent_routes(@agent_record.id)
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ module Memory
5
+ # Per-agent private memory storage
6
+ class AgentMemory < Base
7
+ attr_reader :agent
8
+
9
+ def initialize(agent)
10
+ super()
11
+ @agent = agent
12
+ end
13
+
14
+ # Get a value from agent memory
15
+ def get(key)
16
+ fetch_with_cache(key) do
17
+ return nil unless defined?(::AgentMemory)
18
+
19
+ memory = @agent.agent_memories.active.find_by(key: key)
20
+ memory&.value
21
+ end
22
+ end
23
+
24
+ # Set a value in agent memory
25
+ def set(key, value, expires_in: nil)
26
+ return false unless defined?(::AgentMemory)
27
+
28
+ write_through(key, value, expires_in: expires_in) do
29
+ memory = @agent.agent_memories.find_or_initialize_by(key: key)
30
+ memory.value = value
31
+ memory.expires_at = expires_in.present? ? Time.current + expires_in : nil
32
+ memory.save!
33
+ end
34
+ end
35
+
36
+ # Check if a key exists
37
+ def exists?(key)
38
+ return false unless defined?(::AgentMemory)
39
+
40
+ if @cache_enabled && Rails.cache.exist?(cache_key(key))
41
+ true
42
+ else
43
+ @agent.agent_memories.active.exists?(key: key)
44
+ end
45
+ end
46
+
47
+ # Delete a key
48
+ def delete(key)
49
+ return false unless defined?(::AgentMemory)
50
+
51
+ delete_through(key) do
52
+ @agent.agent_memories.where(key: key).destroy_all.any?
53
+ end
54
+ end
55
+
56
+ # Get all keys
57
+ def keys
58
+ return [] unless defined?(::AgentMemory)
59
+
60
+ @agent.agent_memories.active.pluck(:key)
61
+ end
62
+
63
+ # Get all memory as hash
64
+ def all
65
+ return {} unless defined?(::AgentMemory)
66
+
67
+ @agent.agent_memories.active.pluck(:key, :value).to_h
68
+ end
69
+
70
+ # Clear all agent memory
71
+ def clear!
72
+ return false unless defined?(::AgentMemory)
73
+
74
+ @agent.agent_memories.destroy_all
75
+
76
+ # Clear cache entries
77
+ keys.each { |key| Rails.cache.delete(cache_key(key)) } if @cache_enabled
78
+
79
+ true
80
+ end
81
+
82
+ # Remember something with optional TTL
83
+ def remember(key, expires_in: nil)
84
+ value = get(key)
85
+ return value if value.present?
86
+
87
+ value = yield
88
+ set(key, value, expires_in: expires_in) if value.present?
89
+ value
90
+ end
91
+
92
+ # Increment a counter
93
+ def increment(key, amount = 1)
94
+ current = get(key) || 0
95
+ new_value = current + amount
96
+ set(key, new_value)
97
+ new_value
98
+ end
99
+
100
+ # Decrement a counter
101
+ def decrement(key, amount = 1)
102
+ increment(key, -amount)
103
+ end
104
+
105
+ # Add to a list
106
+ def push(key, value)
107
+ list = get(key) || []
108
+ list << value
109
+ set(key, list)
110
+ list
111
+ end
112
+
113
+ # Remove from a list
114
+ def pull(key, value)
115
+ list = get(key) || []
116
+ list.delete(value)
117
+ set(key, list)
118
+ list
119
+ end
120
+
121
+ protected
122
+
123
+ def cache_key(key)
124
+ "agent_memory/#{@agent.id}/#{key}"
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ module Memory
5
+ # Base class for memory implementations
6
+ class Base
7
+ include ActiveMatrix::Logging
8
+
9
+ def initialize
10
+ @cache_enabled = Rails.cache.present? rescue false
11
+ end
12
+
13
+ # Get a value from memory
14
+ def get(key)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Set a value in memory
19
+ def set(key, value, expires_in: nil)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # Check if a key exists
24
+ def exists?(key)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ # Delete a key
29
+ def delete(key)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ # Get multiple keys at once
34
+ def get_multi(*keys)
35
+ keys.to_h { |key| [key, get(key)] }
36
+ end
37
+
38
+ # Set multiple keys at once
39
+ def set_multi(hash, expires_in: nil)
40
+ hash.each { |key, value| set(key, value, expires_in: expires_in) }
41
+ end
42
+
43
+ # Clear all memory (use with caution)
44
+ def clear!
45
+ raise NotImplementedError
46
+ end
47
+
48
+ protected
49
+
50
+ # Generate cache key
51
+ def cache_key(key)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ # Try cache first, then database
56
+ def fetch_with_cache(key, expires_in: nil)
57
+ return yield unless @cache_enabled
58
+
59
+ cached_key = cache_key(key)
60
+
61
+ # Try cache first
62
+ cached = Rails.cache.read(cached_key)
63
+ return cached if cached.present?
64
+
65
+ # Get from source
66
+ value = yield
67
+ return nil if value.nil?
68
+
69
+ # Write to cache
70
+ Rails.cache.write(cached_key, value, expires_in: expires_in)
71
+ value
72
+ end
73
+
74
+ # Write through to cache and database
75
+ def write_through(key, value, expires_in: nil)
76
+ cached_key = cache_key(key)
77
+
78
+ # Write to database first
79
+ result = yield
80
+
81
+ # Then update cache if enabled
82
+ Rails.cache.write(cached_key, value, expires_in: expires_in) if @cache_enabled && result
83
+
84
+ result
85
+ end
86
+
87
+ # Delete from cache and database
88
+ def delete_through(key)
89
+ cached_key = cache_key(key)
90
+
91
+ # Delete from database
92
+ result = yield
93
+
94
+ # Delete from cache if enabled
95
+ Rails.cache.delete(cached_key) if @cache_enabled
96
+
97
+ result
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ module Memory
5
+ # Per-conversation memory storage (speaker-specific)
6
+ class ConversationMemory < Base
7
+ attr_reader :agent, :user_id, :room_id
8
+
9
+ def initialize(agent, user_id, room_id)
10
+ super()
11
+ @agent = agent
12
+ @user_id = user_id
13
+ @room_id = room_id
14
+ end
15
+
16
+ # Get conversation context
17
+ def context
18
+ fetch_with_cache('context', expires_in: 1.hour) do
19
+ return {} unless defined?(::ConversationContext)
20
+
21
+ record = find_or_create_record
22
+ record.context
23
+ end
24
+ end
25
+
26
+ # Update conversation context
27
+ def update_context(data)
28
+ return false unless defined?(::ConversationContext)
29
+
30
+ record = find_or_create_record
31
+ record.context = record.context.merge(data)
32
+ record.save!
33
+
34
+ # Update cache
35
+ invalidate_cache
36
+ true
37
+ end
38
+
39
+ # Add a message to history
40
+ def add_message(event)
41
+ return false unless defined?(::ConversationContext)
42
+
43
+ record = find_or_create_record
44
+ record.add_message({
45
+ event_id: event[:event_id],
46
+ sender: event[:sender],
47
+ content: event.dig(:content, :body),
48
+ timestamp: event[:origin_server_ts] || (Time.current.to_i * 1000)
49
+ })
50
+
51
+ # Update agent activity
52
+ @agent.update_activity!
53
+ @agent.increment_messages_handled!
54
+
55
+ # Invalidate cache
56
+ invalidate_cache
57
+ true
58
+ end
59
+
60
+ # Get recent messages
61
+ def recent_messages(limit = 10)
62
+ fetch_with_cache('recent_messages', expires_in: 5.minutes) do
63
+ return [] unless defined?(::ConversationContext)
64
+
65
+ record = find_or_create_record
66
+ record.recent_messages(limit)
67
+ end
68
+ end
69
+
70
+ # Get last message timestamp
71
+ def last_message_at
72
+ return nil unless defined?(::ConversationContext)
73
+
74
+ record = conversation_record
75
+ record&.last_message_at
76
+ end
77
+
78
+ # Check if conversation is active (recent activity)
79
+ def active?
80
+ last_at = last_message_at
81
+ last_at.present? && last_at > 1.hour.ago
82
+ end
83
+
84
+ # Clear conversation history but keep context
85
+ def clear_history!
86
+ return false unless defined?(::ConversationContext)
87
+
88
+ record = conversation_record
89
+ return false unless record
90
+
91
+ record.prune_history!
92
+ invalidate_cache
93
+ true
94
+ end
95
+
96
+ # Get or set a specific context value
97
+ def [](key)
98
+ context[key.to_s]
99
+ end
100
+
101
+ def []=(key, value)
102
+ update_context(key.to_s => value)
103
+ end
104
+
105
+ # Remember something in conversation context
106
+ def remember(key)
107
+ value = self[key]
108
+ return value if value.present?
109
+
110
+ value = yield
111
+ self[key] = value if value.present?
112
+ value
113
+ end
114
+
115
+ # Get conversation summary
116
+ def summary
117
+ {
118
+ user_id: @user_id,
119
+ room_id: @room_id,
120
+ active: active?,
121
+ message_count: conversation_record&.message_count || 0,
122
+ last_message_at: last_message_at,
123
+ context: context
124
+ }
125
+ end
126
+
127
+ protected
128
+
129
+ def cache_key(suffix)
130
+ "conversation/#{@agent.id}/#{@user_id}/#{@room_id}/#{suffix}"
131
+ end
132
+
133
+ def conversation_record
134
+ return nil unless defined?(::ConversationContext)
135
+
136
+ ::ConversationContext.find_by(
137
+ matrix_agent: @agent,
138
+ user_id: @user_id,
139
+ room_id: @room_id
140
+ )
141
+ end
142
+
143
+ def find_or_create_record
144
+ return nil unless defined?(::ConversationContext)
145
+
146
+ ::ConversationContext.find_or_create_by!(
147
+ matrix_agent: @agent,
148
+ user_id: @user_id,
149
+ room_id: @room_id
150
+ )
151
+ end
152
+
153
+ def invalidate_cache
154
+ return unless @cache_enabled
155
+
156
+ Rails.cache.delete(cache_key('context'))
157
+ Rails.cache.delete(cache_key('recent_messages'))
158
+ end
159
+ end
160
+ end
161
+ end