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.
- checksums.yaml +4 -4
- data/README.md +96 -28
- data/app/models/active_matrix/agent.rb +36 -1
- data/app/models/active_matrix/agent_store.rb +29 -0
- data/app/models/active_matrix/application_record.rb +8 -0
- data/app/models/active_matrix/chat_session.rb +29 -0
- data/app/models/active_matrix/knowledge_base.rb +26 -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 +7 -3
- 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/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +12 -2
- data/lib/generators/active_matrix/install/install_generator.rb +3 -15
- data/lib/generators/active_matrix/install/templates/README +5 -2
- data/lib/generators/active_matrix/install/templates/active_matrix.yml +32 -0
- metadata +142 -45
- data/lib/active_matrix/protocols/cs/message_relationships.rb +0 -318
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
::
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
::
|
|
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
|
-
::
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
scope = ::GlobalMemory.active
|
|
47
|
+
scope = ActiveMatrix::KnowledgeBase.active
|
|
58
48
|
scope = scope.by_category(category) if category
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = ::
|
|
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(' ', ' ')
|
|
190
|
+
text = text.gsub('<', '<')
|
|
191
|
+
text = text.gsub('>', '>')
|
|
192
|
+
text = text.gsub('&', '&')
|
|
193
|
+
text = text.gsub('"', '"')
|
|
194
|
+
text.strip
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|