activematrix 0.0.7 → 0.0.9

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -28
  3. data/app/models/active_matrix/agent.rb +36 -1
  4. data/app/models/active_matrix/agent_store.rb +29 -0
  5. data/app/models/active_matrix/application_record.rb +8 -0
  6. data/app/models/active_matrix/chat_session.rb +29 -0
  7. data/app/models/active_matrix/knowledge_base.rb +26 -0
  8. data/exe/activematrix +7 -0
  9. data/lib/active_matrix/agent_manager.rb +160 -121
  10. data/lib/active_matrix/agent_registry.rb +25 -21
  11. data/lib/active_matrix/api.rb +8 -2
  12. data/lib/active_matrix/async_query.rb +58 -0
  13. data/lib/active_matrix/bot/base.rb +3 -3
  14. data/lib/active_matrix/bot/builtin_commands.rb +188 -0
  15. data/lib/active_matrix/bot/command_parser.rb +175 -0
  16. data/lib/active_matrix/cli.rb +273 -0
  17. data/lib/active_matrix/client.rb +21 -6
  18. data/lib/active_matrix/client_pool.rb +38 -27
  19. data/lib/active_matrix/daemon/probe_server.rb +118 -0
  20. data/lib/active_matrix/daemon/signal_handler.rb +156 -0
  21. data/lib/active_matrix/daemon/worker.rb +109 -0
  22. data/lib/active_matrix/daemon.rb +236 -0
  23. data/lib/active_matrix/engine.rb +7 -3
  24. data/lib/active_matrix/errors.rb +1 -1
  25. data/lib/active_matrix/event_router.rb +61 -49
  26. data/lib/active_matrix/events.rb +1 -0
  27. data/lib/active_matrix/instrumentation.rb +148 -0
  28. data/lib/active_matrix/memory/agent_memory.rb +7 -21
  29. data/lib/active_matrix/memory/conversation_memory.rb +4 -20
  30. data/lib/active_matrix/memory/global_memory.rb +15 -30
  31. data/lib/active_matrix/message_dispatcher.rb +197 -0
  32. data/lib/active_matrix/metrics.rb +424 -0
  33. data/lib/active_matrix/presence_manager.rb +181 -0
  34. data/lib/active_matrix/telemetry.rb +134 -0
  35. data/lib/active_matrix/version.rb +1 -1
  36. data/lib/active_matrix.rb +12 -2
  37. data/lib/generators/active_matrix/install/install_generator.rb +3 -15
  38. data/lib/generators/active_matrix/install/templates/README +5 -2
  39. data/lib/generators/active_matrix/install/templates/active_matrix.yml +32 -0
  40. metadata +142 -45
  41. data/lib/active_matrix/protocols/cs/message_relationships.rb +0 -318
  42. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
  43. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
  44. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
  45. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
@@ -16,8 +16,6 @@ module ActiveMatrix
16
16
  # Get conversation context
17
17
  def context
18
18
  fetch_with_cache('context', expires_in: 1.hour) do
19
- return {} unless defined?(::ConversationContext)
20
-
21
19
  record = find_or_create_record
22
20
  record.context
23
21
  end
@@ -25,8 +23,6 @@ module ActiveMatrix
25
23
 
26
24
  # Update conversation context
27
25
  def update_context(data)
28
- return false unless defined?(::ConversationContext)
29
-
30
26
  record = find_or_create_record
31
27
  record.context = record.context.merge(data)
32
28
  record.save!
@@ -38,8 +34,6 @@ module ActiveMatrix
38
34
 
39
35
  # Add a message to history
40
36
  def add_message(event)
41
- return false unless defined?(::ConversationContext)
42
-
43
37
  record = find_or_create_record
44
38
  record.add_message({
45
39
  event_id: event[:event_id],
@@ -60,8 +54,6 @@ module ActiveMatrix
60
54
  # Get recent messages
61
55
  def recent_messages(limit = 10)
62
56
  fetch_with_cache('recent_messages', expires_in: 5.minutes) do
63
- return [] unless defined?(::ConversationContext)
64
-
65
57
  record = find_or_create_record
66
58
  record.recent_messages(limit)
67
59
  end
@@ -69,8 +61,6 @@ module ActiveMatrix
69
61
 
70
62
  # Get last message timestamp
71
63
  def last_message_at
72
- return nil unless defined?(::ConversationContext)
73
-
74
64
  record = conversation_record
75
65
  record&.last_message_at
76
66
  end
@@ -83,8 +73,6 @@ module ActiveMatrix
83
73
 
84
74
  # Clear conversation history but keep context
85
75
  def clear_history!
86
- return false unless defined?(::ConversationContext)
87
-
88
76
  record = conversation_record
89
77
  return false unless record
90
78
 
@@ -131,20 +119,16 @@ module ActiveMatrix
131
119
  end
132
120
 
133
121
  def conversation_record
134
- return nil unless defined?(::ConversationContext)
135
-
136
- ::ConversationContext.find_by(
137
- matrix_agent: @agent,
122
+ ActiveMatrix::ChatSession.find_by(
123
+ agent: @agent,
138
124
  user_id: @user_id,
139
125
  room_id: @room_id
140
126
  )
141
127
  end
142
128
 
143
129
  def find_or_create_record
144
- return nil unless defined?(::ConversationContext)
145
-
146
- ::ConversationContext.find_or_create_by!(
147
- matrix_agent: @agent,
130
+ ActiveMatrix::ChatSession.find_or_create_by!(
131
+ agent: @agent,
148
132
  user_id: @user_id,
149
133
  room_id: @room_id
150
134
  )
@@ -11,74 +11,59 @@ module ActiveMatrix
11
11
  # Get a value from global memory
12
12
  def get(key)
13
13
  fetch_with_cache(key) do
14
- return nil unless defined?(::GlobalMemory)
15
-
16
- ::GlobalMemory.get(key)
14
+ ActiveMatrix::KnowledgeBase.get(key)
17
15
  end
18
16
  end
19
17
 
20
18
  # Set a value in global memory
21
19
  def set(key, value, category: nil, expires_in: nil, public_read: true, public_write: false)
22
- return false unless defined?(::GlobalMemory)
23
-
24
20
  write_through(key, value, expires_in: expires_in) do
25
- ::GlobalMemory.set(key, value,
26
- category: category,
27
- expires_in: expires_in,
28
- public_read: public_read,
29
- public_write: public_write)
21
+ ActiveMatrix::KnowledgeBase.set(key, value,
22
+ category: category,
23
+ expires_in: expires_in,
24
+ public_read: public_read,
25
+ public_write: public_write)
30
26
  end
31
27
  end
32
28
 
33
29
  # Check if a key exists
34
30
  def exists?(key)
35
- return false unless defined?(::GlobalMemory)
36
-
37
31
  if @cache_enabled && Rails.cache.exist?(cache_key(key))
38
32
  true
39
33
  else
40
- ::GlobalMemory.active.exists?(key: key)
34
+ ActiveMatrix::KnowledgeBase.active.exists?(key: key)
41
35
  end
42
36
  end
43
37
 
44
38
  # Delete a key
45
39
  def delete(key)
46
- return false unless defined?(::GlobalMemory)
47
-
48
40
  delete_through(key) do
49
- ::GlobalMemory.where(key: key).destroy_all.any?
41
+ ActiveMatrix::KnowledgeBase.where(key: key).destroy_all.any?
50
42
  end
51
43
  end
52
44
 
53
45
  # Get all keys in a category
54
46
  def keys(category: nil)
55
- return [] unless defined?(::GlobalMemory)
56
-
57
- scope = ::GlobalMemory.active
47
+ scope = ActiveMatrix::KnowledgeBase.active
58
48
  scope = scope.by_category(category) if category
59
- scope.pluck(:key)
49
+ AsyncQuery.async_pluck(scope, :key)
60
50
  end
61
51
 
62
52
  # Get all values in a category
63
53
  def by_category(category)
64
- return {} unless defined?(::GlobalMemory)
65
-
66
- ::GlobalMemory.active.by_category(category).pluck(:key, :value).to_h
54
+ scope = ActiveMatrix::KnowledgeBase.active.by_category(category)
55
+ AsyncQuery.async_pluck(scope, :key, :value).to_h
67
56
  end
68
57
 
69
58
  # Check if readable by agent
70
59
  def readable?(key, agent = nil)
71
- return false unless defined?(::GlobalMemory)
72
-
73
- memory = ::GlobalMemory.find_by(key: key)
60
+ memory = ActiveMatrix::KnowledgeBase.find_by(key: key)
74
61
  memory&.readable_by?(agent)
75
62
  end
76
63
 
77
64
  # Check if writable by agent
78
65
  def writable?(key, agent = nil)
79
- return false unless defined?(::GlobalMemory)
80
-
81
- memory = ::GlobalMemory.find_by(key: key)
66
+ memory = ActiveMatrix::KnowledgeBase.find_by(key: key)
82
67
  memory&.writable_by?(agent)
83
68
  end
84
69
 
@@ -92,7 +77,7 @@ module ActiveMatrix
92
77
  # Set with permission check
93
78
  def set_for_agent(key, value, agent, **)
94
79
  # Allow creating new keys or updating writable ones
95
- memory = ::GlobalMemory.find_by(key: key)
80
+ memory = ActiveMatrix::KnowledgeBase.find_by(key: key)
96
81
  return false if memory && !memory.writable_by?(agent)
97
82
 
98
83
  set(key, value, **)
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ # Dispatches Matrix messages with retry logic and typing indicators
5
+ #
6
+ # @example Basic usage
7
+ # dispatcher = ActiveMatrix::MessageDispatcher.new(api: api, room_id: '!abc:matrix.org')
8
+ # dispatcher.send_text('Hello!')
9
+ #
10
+ # @example With typing indicator
11
+ # dispatcher.send_text('Thinking...', typing_delay: 2.0)
12
+ #
13
+ # @example Thread reply
14
+ # dispatcher.send_text('Reply', thread_id: '$event_id')
15
+ #
16
+ class MessageDispatcher
17
+ include Instrumentation
18
+
19
+ # Default configuration
20
+ DEFAULT_RETRY_COUNT = 3
21
+ DEFAULT_BASE_DELAY = 1.0
22
+ DEFAULT_TYPING_DELAY = 0.5
23
+ DEFAULT_TYPING_TIMEOUT = 30
24
+
25
+ attr_reader :api, :room_id, :user_id
26
+
27
+ # @param api [ActiveMatrix::Api] Matrix API instance
28
+ # @param room_id [String] Room ID to send messages to
29
+ # @param user_id [String] User ID for typing indicator
30
+ # @param retry_count [Integer] Number of retries on failure
31
+ # @param base_delay [Float] Base delay in seconds for exponential backoff
32
+ # @param typing_delay [Float] Default typing delay in seconds
33
+ def initialize(api:, room_id:, user_id:, retry_count: DEFAULT_RETRY_COUNT,
34
+ base_delay: DEFAULT_BASE_DELAY, typing_delay: DEFAULT_TYPING_DELAY)
35
+ @api = api
36
+ @room_id = room_id
37
+ @user_id = user_id
38
+ @retry_count = retry_count
39
+ @base_delay = base_delay
40
+ @default_typing_delay = typing_delay
41
+ end
42
+
43
+ # Send a plain text message
44
+ #
45
+ # @param text [String] Message text
46
+ # @param msgtype [String] Message type (default: 'm.text')
47
+ # @param typing_delay [Float, nil] Seconds to show typing indicator (nil to skip)
48
+ # @param thread_id [String, nil] Event ID to reply in thread
49
+ # @return [Hash] Response with :event_id
50
+ def send_text(text, msgtype: 'm.text', typing_delay: nil, thread_id: nil)
51
+ content = {
52
+ msgtype: msgtype,
53
+ body: text
54
+ }
55
+
56
+ send_with_typing(content, typing_delay: typing_delay, thread_id: thread_id)
57
+ end
58
+
59
+ # Send an HTML message
60
+ #
61
+ # @param html [String] HTML content
62
+ # @param body [String, nil] Plain text fallback (auto-generated if nil)
63
+ # @param msgtype [String] Message type (default: 'm.text')
64
+ # @param typing_delay [Float, nil] Seconds to show typing indicator
65
+ # @param thread_id [String, nil] Event ID to reply in thread
66
+ # @return [Hash] Response with :event_id
67
+ def send_html(html, body: nil, msgtype: 'm.text', typing_delay: nil, thread_id: nil)
68
+ plain_body = body || strip_html(html)
69
+
70
+ content = {
71
+ msgtype: msgtype,
72
+ body: plain_body,
73
+ format: 'org.matrix.custom.html',
74
+ formatted_body: html
75
+ }
76
+
77
+ send_with_typing(content, typing_delay: typing_delay, thread_id: thread_id)
78
+ end
79
+
80
+ # Send a notice message (typically for bot responses)
81
+ #
82
+ # @param text [String] Notice text
83
+ # @param typing_delay [Float, nil] Seconds to show typing indicator
84
+ # @param thread_id [String, nil] Event ID to reply in thread
85
+ # @return [Hash] Response with :event_id
86
+ def send_notice(text, typing_delay: nil, thread_id: nil)
87
+ send_text(text, msgtype: 'm.notice', typing_delay: typing_delay, thread_id: thread_id)
88
+ end
89
+
90
+ # Send an HTML notice message
91
+ #
92
+ # @param html [String] HTML content
93
+ # @param body [String, nil] Plain text fallback
94
+ # @param typing_delay [Float, nil] Seconds to show typing indicator
95
+ # @param thread_id [String, nil] Event ID to reply in thread
96
+ # @return [Hash] Response with :event_id
97
+ def send_html_notice(html, body: nil, typing_delay: nil, thread_id: nil)
98
+ send_html(html, body: body, msgtype: 'm.notice', typing_delay: typing_delay, thread_id: thread_id)
99
+ end
100
+
101
+ # Send an emote message (/me action)
102
+ #
103
+ # @param text [String] Emote text
104
+ # @param typing_delay [Float, nil] Seconds to show typing indicator
105
+ # @param thread_id [String, nil] Event ID to reply in thread
106
+ # @return [Hash] Response with :event_id
107
+ def send_emote(text, typing_delay: nil, thread_id: nil)
108
+ send_text(text, msgtype: 'm.emote', typing_delay: typing_delay, thread_id: thread_id)
109
+ end
110
+
111
+ # Show typing indicator
112
+ #
113
+ # @param typing [Boolean] Whether to show or hide typing
114
+ # @param timeout [Integer] Timeout in seconds
115
+ def set_typing(typing: true, timeout: DEFAULT_TYPING_TIMEOUT)
116
+ @api.set_typing(@room_id, @user_id, typing: typing, timeout: timeout)
117
+ rescue StandardError => e
118
+ ActiveMatrix.logger.debug("Failed to set typing indicator: #{e.message}")
119
+ end
120
+
121
+ private
122
+
123
+ def agent_id
124
+ @user_id
125
+ end
126
+
127
+ def send_with_typing(content, typing_delay:, thread_id:)
128
+ effective_delay = typing_delay || @default_typing_delay
129
+
130
+ # Show typing indicator
131
+ if effective_delay.positive?
132
+ set_typing(typing: true)
133
+ sleep(effective_delay)
134
+ set_typing(typing: false)
135
+ end
136
+
137
+ # Add thread relation if specified
138
+ if thread_id
139
+ content[:'m.relates_to'] = {
140
+ rel_type: 'm.thread',
141
+ event_id: thread_id
142
+ }
143
+ end
144
+
145
+ send_with_retry(content)
146
+ end
147
+
148
+ def send_with_retry(content)
149
+ attempts = 0
150
+
151
+ instrument_operation(:send_message, room_id: @room_id) do
152
+ @api.send_message_event(@room_id, 'm.room.message', content)
153
+ rescue ActiveMatrix::MatrixRequestError => e
154
+ attempts += 1
155
+
156
+ if attempts <= @retry_count && retryable_error?(e)
157
+ delay = calculate_backoff(attempts)
158
+ ActiveMatrix.logger.warn("Message send failed (attempt #{attempts}/#{@retry_count}), retrying in #{delay}s: #{e.message}")
159
+ sleep(delay)
160
+ retry
161
+ end
162
+
163
+ raise
164
+ end
165
+ end
166
+
167
+ def retryable_error?(error)
168
+ # Retry on rate limiting, server errors, or network issues
169
+ case error
170
+ when ActiveMatrix::MatrixTooManyRequestsError
171
+ true
172
+ when ActiveMatrix::MatrixRequestError
173
+ error.httpstatus.to_i >= 500
174
+ else
175
+ false
176
+ end
177
+ end
178
+
179
+ def calculate_backoff(attempt)
180
+ # Exponential backoff with full jitter
181
+ max_delay = @base_delay * (2**(attempt - 1))
182
+ rand * max_delay
183
+ end
184
+
185
+ def strip_html(html)
186
+ # Simple HTML stripping - remove tags and decode entities
187
+ text = html.gsub(/<br\s*\/?>/i, "\n")
188
+ text = text.gsub(/<\/?[^>]+>/, '')
189
+ text = text.gsub('&nbsp;', ' ')
190
+ text = text.gsub('&lt;', '<')
191
+ text = text.gsub('&gt;', '>')
192
+ text = text.gsub('&amp;', '&')
193
+ text = text.gsub('&quot;', '"')
194
+ text.strip
195
+ end
196
+ end
197
+ end