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.
- checksums.yaml +4 -4
- data/README.md +218 -51
- data/lib/active_matrix/agent_manager.rb +275 -0
- data/lib/active_matrix/agent_registry.rb +154 -0
- data/lib/active_matrix/bot/multi_instance_base.rb +189 -0
- data/lib/active_matrix/client.rb +5 -15
- data/lib/active_matrix/client_pool.rb +194 -0
- data/lib/active_matrix/event_router.rb +215 -0
- data/lib/active_matrix/memory/agent_memory.rb +128 -0
- data/lib/active_matrix/memory/base.rb +101 -0
- data/lib/active_matrix/memory/conversation_memory.rb +161 -0
- data/lib/active_matrix/memory/global_memory.rb +153 -0
- data/lib/active_matrix/memory.rb +28 -0
- data/lib/active_matrix/room.rb +131 -51
- data/lib/active_matrix/rooms/space.rb +1 -5
- data/lib/active_matrix/user.rb +10 -0
- data/lib/active_matrix/util/account_data_cache.rb +62 -24
- data/lib/active_matrix/util/cacheable.rb +73 -0
- data/lib/active_matrix/util/extensions.rb +4 -0
- data/lib/active_matrix/util/state_event_cache.rb +106 -31
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +51 -3
- data/lib/generators/active_matrix/bot/bot_generator.rb +38 -0
- data/lib/generators/active_matrix/bot/templates/bot.rb.erb +111 -0
- data/lib/generators/active_matrix/bot/templates/bot_spec.rb.erb +68 -0
- data/lib/generators/active_matrix/install/install_generator.rb +44 -0
- data/lib/generators/active_matrix/install/templates/README +30 -0
- data/lib/generators/active_matrix/install/templates/active_matrix.rb +33 -0
- data/lib/generators/active_matrix/install/templates/agent_memory.rb +47 -0
- data/lib/generators/active_matrix/install/templates/conversation_context.rb +72 -0
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +17 -0
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +21 -0
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +20 -0
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +26 -0
- data/lib/generators/active_matrix/install/templates/global_memory.rb +70 -0
- data/lib/generators/active_matrix/install/templates/matrix_agent.rb +127 -0
- metadata +110 -4
- data/lib/active_matrix/util/rails_cache_adapter.rb +0 -37
- data/lib/active_matrix/util/tinycache.rb +0 -145
- data/lib/active_matrix/util/tinycache_adapter.rb +0 -87
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix
|
4
|
+
module Util
|
5
|
+
# Provides caching functionality for Matrix objects
|
6
|
+
# Handles serialization/deserialization to work with Rails.cache
|
7
|
+
module Cacheable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
attr_accessor :cached_at
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
# Reconstruct object from cached data
|
16
|
+
def from_cache(client, data)
|
17
|
+
return nil unless data.is_a?(Hash) && data[:_cache_class] == name
|
18
|
+
|
19
|
+
# Remove cache metadata
|
20
|
+
attrs = data.except(:_cache_class, :_cached_at)
|
21
|
+
|
22
|
+
# Reconstruct based on class type
|
23
|
+
case name
|
24
|
+
when 'ActiveMatrix::User'
|
25
|
+
new(client, attrs[:id], attrs.except(:id))
|
26
|
+
when 'ActiveMatrix::Room'
|
27
|
+
new(client, attrs[:id], attrs.except(:id))
|
28
|
+
else
|
29
|
+
new(client, attrs)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert object to cacheable hash
|
35
|
+
def to_cache
|
36
|
+
data = cache_attributes.merge(
|
37
|
+
_cache_class: self.class.name,
|
38
|
+
_cached_at: Time.current
|
39
|
+
)
|
40
|
+
|
41
|
+
# Ensure we only cache serializable data
|
42
|
+
data.deep_stringify_keys
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override in each class to specify what to cache
|
46
|
+
def cache_attributes
|
47
|
+
if respond_to?(:attributes)
|
48
|
+
attributes
|
49
|
+
elsif respond_to?(:to_h)
|
50
|
+
to_h
|
51
|
+
else
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Generate a cache key for this object
|
57
|
+
def cache_key(*suffixes)
|
58
|
+
base_key = "#{self.class.name.underscore}:#{cache_id}"
|
59
|
+
suffixes.any? ? "#{base_key}:#{suffixes.join(':')}" : base_key
|
60
|
+
end
|
61
|
+
|
62
|
+
# Override in each class if ID method is different
|
63
|
+
def cache_id
|
64
|
+
respond_to?(:id) ? id : object_id
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if this object was loaded from cache
|
68
|
+
def from_cache?
|
69
|
+
@cached_at.present?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -3,14 +3,13 @@
|
|
3
3
|
module ActiveMatrix::Util
|
4
4
|
class StateEventCache
|
5
5
|
extend ActiveMatrix::Extensions
|
6
|
-
extend ActiveMatrix::Util::Tinycache
|
7
6
|
include Enumerable
|
8
7
|
|
9
8
|
attr_reader :room
|
10
9
|
|
11
10
|
attr_accessor :cache_time
|
12
11
|
|
13
|
-
ignore_inspect :client, :room
|
12
|
+
ignore_inspect :client, :room
|
14
13
|
|
15
14
|
def initialize(room, cache_time: 30 * 60, **_params)
|
16
15
|
raise ArgumentError, 'Must be given a Room instance' unless room.is_a? ActiveMatrix::Room
|
@@ -24,21 +23,18 @@ module ActiveMatrix::Util
|
|
24
23
|
end
|
25
24
|
|
26
25
|
def reload!
|
27
|
-
|
26
|
+
# Clear all cache entries for this room's state
|
27
|
+
cache.delete_matched("activematrix:room:#{room.id}:state:*") if cache_available?
|
28
28
|
end
|
29
29
|
|
30
30
|
def keys
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
state_key = nil if state_key == real_type
|
35
|
-
|
36
|
-
[real_type, state_key]
|
37
|
-
end
|
31
|
+
# State is not enumerable when using Rails.cache
|
32
|
+
# This would require keeping track of keys separately
|
33
|
+
[]
|
38
34
|
end
|
39
35
|
|
40
36
|
def values
|
41
|
-
|
37
|
+
[]
|
42
38
|
end
|
43
39
|
|
44
40
|
def size
|
@@ -46,47 +42,126 @@ module ActiveMatrix::Util
|
|
46
42
|
end
|
47
43
|
|
48
44
|
def key?(type, key = nil)
|
49
|
-
|
45
|
+
cache_available? && cache.exist?(cache_key(type, key))
|
50
46
|
end
|
51
47
|
|
52
48
|
def expire(type, key = nil)
|
53
|
-
|
49
|
+
cache.delete(cache_key(type, key)) if cache_available?
|
54
50
|
end
|
55
51
|
|
56
52
|
def each(live: false)
|
57
|
-
|
58
|
-
|
59
|
-
keys.each do |type|
|
60
|
-
real_type = type.split('|').first
|
61
|
-
state_key = type.split('|').last
|
62
|
-
state_key = nil if state_key == real_type
|
63
|
-
|
64
|
-
v = live ? self[real_type, key: state_key] : tinycache_adapter.read(type)
|
65
|
-
# hash = v.hash
|
66
|
-
yield [real_type, state_key], v
|
67
|
-
# self[key] = v if hash != v.hash
|
68
|
-
end
|
53
|
+
to_enum(__method__, live: live) { 0 } unless block_given?
|
54
|
+
# Not enumerable with Rails.cache
|
69
55
|
end
|
70
56
|
|
71
57
|
def delete(type, key = nil)
|
72
58
|
type = type.to_s unless type.is_a? String
|
73
59
|
client.api.set_room_state(room.id, type, {}, **{ state_key: key }.compact)
|
74
|
-
|
60
|
+
cache.delete(cache_key(type, key)) if cache_available?
|
75
61
|
end
|
76
62
|
|
77
63
|
def [](type, key = nil)
|
78
64
|
type = type.to_s unless type.is_a? String
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
65
|
+
return fetch_state(type, key) if !cache_available? || client.cache == :none
|
66
|
+
|
67
|
+
begin
|
68
|
+
cached_value = cache.fetch(cache_key(type, key), expires_in: @cache_time) do
|
69
|
+
result = fetch_state(type, key)
|
70
|
+
|
71
|
+
# Convert Response objects to plain hashes for caching
|
72
|
+
# Response objects extend Hash but contain an @api instance variable that can't be serialized
|
73
|
+
if result.is_a?(Hash)
|
74
|
+
# Create a clean hash with just the data, no instance variables or extended modules
|
75
|
+
# Deep convert to ensure no mock objects are included
|
76
|
+
clean_hash = {}
|
77
|
+
result.each do |key, value|
|
78
|
+
clean_hash[key] = case value
|
79
|
+
when Hash then value.to_h
|
80
|
+
when Array then value.map { |v| v.is_a?(Hash) ? v.to_h : v }
|
81
|
+
else value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
clean_hash
|
85
|
+
else
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# If it's a hash and we have an API client, convert it back to a Response
|
91
|
+
if cached_value.is_a?(Hash) && !cached_value.empty? && client.respond_to?(:api)
|
92
|
+
ActiveMatrix::Response.new(client.api, cached_value)
|
93
|
+
else
|
94
|
+
cached_value
|
95
|
+
end
|
96
|
+
rescue StandardError
|
97
|
+
# If caching fails, return the direct result
|
98
|
+
fetch_state(type, key)
|
83
99
|
end
|
84
100
|
end
|
85
101
|
|
102
|
+
def fetch_state(type, key = nil)
|
103
|
+
client.api.get_room_state(room.id, type, **{ key: key }.compact)
|
104
|
+
rescue ActiveMatrix::MatrixNotFoundError
|
105
|
+
{}
|
106
|
+
end
|
107
|
+
|
86
108
|
def []=(type, key = nil, value) # rubocop:disable Style/OptionalArguments Not possible to put optional last
|
87
109
|
type = type.to_s unless type.is_a? String
|
88
110
|
client.api.set_room_state(room.id, type, value, **{ state_key: key }.compact)
|
89
|
-
|
111
|
+
|
112
|
+
return unless cache_available?
|
113
|
+
|
114
|
+
# Convert to plain hash for caching to avoid serialization issues with Mocha
|
115
|
+
cacheable_value = if value.is_a?(Hash)
|
116
|
+
clean_hash = {}
|
117
|
+
value.each do |k, v|
|
118
|
+
clean_hash[k] = case v
|
119
|
+
when Hash then v.to_h
|
120
|
+
when Array then v.map { |item| item.is_a?(Hash) ? item.to_h : item }
|
121
|
+
else v
|
122
|
+
end
|
123
|
+
end
|
124
|
+
clean_hash
|
125
|
+
else
|
126
|
+
value
|
127
|
+
end
|
128
|
+
cache.write(cache_key(type, key), cacheable_value, expires_in: @cache_time)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Alias for writing without API call
|
132
|
+
def write(type, value, key = nil)
|
133
|
+
type = type.to_s unless type.is_a? String
|
134
|
+
return unless cache_available?
|
135
|
+
|
136
|
+
# Convert to plain hash for caching to avoid serialization issues
|
137
|
+
cacheable_value = if value.is_a?(Hash)
|
138
|
+
clean_hash = {}
|
139
|
+
value.each do |k, v|
|
140
|
+
clean_hash[k] = case v
|
141
|
+
when Hash then v.to_h
|
142
|
+
when Array then v.map { |item| item.is_a?(Hash) ? item.to_h : item }
|
143
|
+
else v
|
144
|
+
end
|
145
|
+
end
|
146
|
+
clean_hash
|
147
|
+
else
|
148
|
+
value
|
149
|
+
end
|
150
|
+
cache.write(cache_key(type, key), cacheable_value, expires_in: @cache_time)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def cache_key(type, key = nil)
|
156
|
+
"activematrix:room:#{room.id}:state:#{type}#{"|#{key}" if key}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def cache_available?
|
160
|
+
defined?(::Rails) && ::Rails.respond_to?(:cache) && ::Rails.cache
|
161
|
+
end
|
162
|
+
|
163
|
+
def cache
|
164
|
+
::Rails.cache
|
90
165
|
end
|
91
166
|
end
|
92
167
|
end
|
data/lib/active_matrix.rb
CHANGED
@@ -9,25 +9,73 @@ require_relative 'active_matrix/errors'
|
|
9
9
|
|
10
10
|
require 'json'
|
11
11
|
require 'zeitwerk'
|
12
|
+
require 'active_support'
|
13
|
+
require 'active_support/core_ext/integer/time'
|
14
|
+
require 'active_support/core_ext/time/calculations'
|
15
|
+
require 'active_support/core_ext/hash/keys'
|
16
|
+
require 'active_support/core_ext/object/blank'
|
12
17
|
|
13
18
|
module ActiveMatrix
|
19
|
+
# Configuration
|
20
|
+
class << self
|
21
|
+
attr_accessor :config
|
22
|
+
|
23
|
+
def configure
|
24
|
+
@config ||= Configuration.new
|
25
|
+
yield @config if block_given?
|
26
|
+
@config
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Configuration class
|
31
|
+
class Configuration
|
32
|
+
attr_accessor :agent_startup_delay, :max_agents_per_process,
|
33
|
+
:agent_health_check_interval, :conversation_history_limit,
|
34
|
+
:conversation_stale_after, :memory_cleanup_interval,
|
35
|
+
:event_queue_size, :event_processing_timeout,
|
36
|
+
:max_clients_per_homeserver, :client_idle_timeout,
|
37
|
+
:agent_log_level, :log_agent_events
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
# Set defaults
|
41
|
+
@agent_startup_delay = 2
|
42
|
+
@max_agents_per_process = 10
|
43
|
+
@agent_health_check_interval = 30
|
44
|
+
@conversation_history_limit = 20
|
45
|
+
@conversation_stale_after = 86_400 # 1 day
|
46
|
+
@memory_cleanup_interval = 3600 # 1 hour
|
47
|
+
@event_queue_size = 1000
|
48
|
+
@event_processing_timeout = 30
|
49
|
+
@max_clients_per_homeserver = 5
|
50
|
+
@client_idle_timeout = 300 # 5 minutes
|
51
|
+
@agent_log_level = :info
|
52
|
+
@log_agent_events = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
14
56
|
# Set up Zeitwerk loader
|
15
57
|
Loader = Zeitwerk::Loader.for_gem
|
16
|
-
|
58
|
+
|
59
|
+
# Ignore directories that shouldn't be autoloaded
|
60
|
+
Loader.ignore("#{__dir__}/generators")
|
61
|
+
|
17
62
|
# Configure inflections for special cases
|
18
63
|
Loader.inflector.inflect(
|
19
64
|
'mxid' => 'MXID',
|
20
65
|
'uri' => 'URI',
|
21
66
|
'as' => 'AS',
|
22
67
|
'cs' => 'CS',
|
23
|
-
'is' => 'IS',
|
68
|
+
'is' => 'IS',
|
24
69
|
'ss' => 'SS',
|
25
70
|
'msc' => 'MSC'
|
26
71
|
)
|
27
72
|
|
28
73
|
# Setup Zeitwerk autoloading
|
29
74
|
Loader.setup
|
30
|
-
|
75
|
+
|
76
|
+
# Eager load all classes if in Rails eager loading mode
|
77
|
+
Loader.eager_load if defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.config&.eager_load
|
78
|
+
|
31
79
|
# Load Railtie for Rails integration
|
32
80
|
require 'active_matrix/railtie' if defined?(Rails::Railtie)
|
33
81
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module ActiveMatrix
|
6
|
+
module Generators
|
7
|
+
class BotGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
desc 'Creates a new ActiveMatrix bot'
|
11
|
+
|
12
|
+
argument :commands, type: :array, default: [], banner: 'command1 command2'
|
13
|
+
|
14
|
+
def create_bot_file
|
15
|
+
template 'bot.rb.erb', "app/bots/#{file_name}_bot.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_bot_spec
|
19
|
+
template 'bot_spec.rb.erb', "spec/bots/#{file_name}_bot_spec.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_usage
|
23
|
+
say "\nBot created! To use your bot:\n\n"
|
24
|
+
say '1. Create an agent in Rails console:'
|
25
|
+
say ' agent = MatrixAgent.create!('
|
26
|
+
say " name: '#{file_name}',"
|
27
|
+
say " homeserver: 'https://matrix.org',"
|
28
|
+
say " username: 'your_bot_username',"
|
29
|
+
say " password: 'your_bot_password',"
|
30
|
+
say " bot_class: '#{class_name}Bot'"
|
31
|
+
say ' )'
|
32
|
+
say "\n2. Start the agent:"
|
33
|
+
say ' ActiveMatrix::AgentManager.instance.start_agent(agent)'
|
34
|
+
say "\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %>Bot < ActiveMatrix::Bot::MultiInstanceBase
|
4
|
+
# Bot configuration
|
5
|
+
set :accept_invites, true
|
6
|
+
set :command_prefix, '!'
|
7
|
+
|
8
|
+
# Set a custom help preamble
|
9
|
+
set :help_preamble, "I am <%= class_name %>, ready to assist!"
|
10
|
+
|
11
|
+
<% if commands.any? %>
|
12
|
+
# Generated commands
|
13
|
+
<% commands.each do |command| %>
|
14
|
+
command :<%= command %>,
|
15
|
+
desc: 'TODO: Add description for <%= command %>',
|
16
|
+
args: 'TODO: Add arguments' do |*args|
|
17
|
+
# TODO: Implement <%= command %> command
|
18
|
+
room.send_notice("Command <%= command %> called with args: #{args.inspect}")
|
19
|
+
|
20
|
+
# Example of using agent memory
|
21
|
+
count = memory.increment("<%= command %>_count")
|
22
|
+
room.send_notice("This command has been used #{count} times")
|
23
|
+
|
24
|
+
# Example of using conversation context
|
25
|
+
context = conversation_context
|
26
|
+
update_context(last_command: '<%= command %>')
|
27
|
+
end
|
28
|
+
|
29
|
+
<% end %>
|
30
|
+
<% else %>
|
31
|
+
# Example command
|
32
|
+
command :hello,
|
33
|
+
desc: 'Say hello',
|
34
|
+
args: '[name]' do |name = nil|
|
35
|
+
greeting = name ? "Hello, #{name}!" : "Hello there!"
|
36
|
+
room.send_notice(greeting)
|
37
|
+
|
38
|
+
# Remember who we greeted in conversation memory
|
39
|
+
if name
|
40
|
+
names_greeted = conversation_memory.remember(:names_greeted) { [] }
|
41
|
+
unless names_greeted.include?(name)
|
42
|
+
names_greeted << name
|
43
|
+
conversation_memory[:names_greeted] = names_greeted
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Example command with agent communication
|
49
|
+
command :broadcast,
|
50
|
+
desc: 'Broadcast a message to all agents',
|
51
|
+
args: 'message' do |*message|
|
52
|
+
msg = message.join(' ')
|
53
|
+
|
54
|
+
# Broadcast to all online agents
|
55
|
+
broadcast_to_agents(:online, {
|
56
|
+
type: 'announcement',
|
57
|
+
message: msg,
|
58
|
+
from_room: room.id
|
59
|
+
})
|
60
|
+
|
61
|
+
room.send_notice("Broadcast sent: #{msg}")
|
62
|
+
end
|
63
|
+
<% end %>
|
64
|
+
|
65
|
+
# Example event handler
|
66
|
+
event 'm.room.member' do
|
67
|
+
if event[:content][:membership] == 'join' && event[:state_key] != client.mxid
|
68
|
+
# Someone joined the room
|
69
|
+
user = client.get_user(event[:state_key])
|
70
|
+
room.send_notice("Welcome, #{user.display_name || user.id}!")
|
71
|
+
|
72
|
+
# Track room members in agent memory
|
73
|
+
members = memory.get(:room_members) || {}
|
74
|
+
members[room.id] ||= []
|
75
|
+
members[room.id] << event[:state_key] unless members[room.id].include?(event[:state_key])
|
76
|
+
memory.set(:room_members, members)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Handle inter-agent messages
|
81
|
+
def receive_message(data, from:)
|
82
|
+
case data[:type]
|
83
|
+
when 'ping'
|
84
|
+
# Respond to ping
|
85
|
+
send_to_agent(from.agent_name, { type: 'pong', timestamp: Time.current })
|
86
|
+
else
|
87
|
+
logger.info "Received message from #{from.agent_name}: #{data.inspect}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Handle broadcasts
|
92
|
+
def receive_broadcast(data, from:)
|
93
|
+
case data[:type]
|
94
|
+
when 'announcement'
|
95
|
+
# Could relay announcements to specific rooms
|
96
|
+
logger.info "Received announcement from #{from.agent_name}: #{data[:message]}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Custom helper methods
|
101
|
+
private
|
102
|
+
|
103
|
+
def greeting_for_time
|
104
|
+
hour = Time.current.hour
|
105
|
+
case hour
|
106
|
+
when 0..11 then "Good morning"
|
107
|
+
when 12..17 then "Good afternoon"
|
108
|
+
when 18..23 then "Good evening"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe <%= class_name %>Bot do
|
6
|
+
let(:agent) do
|
7
|
+
MatrixAgent.create!(
|
8
|
+
name: '<%= file_name %>_test',
|
9
|
+
homeserver: 'https://matrix.example.com',
|
10
|
+
username: 'test_bot',
|
11
|
+
password: 'password',
|
12
|
+
bot_class: '<%= class_name %>Bot'
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:client) { instance_double(ActiveMatrix::Client) }
|
17
|
+
let(:room) { instance_double(ActiveMatrix::Room) }
|
18
|
+
let(:bot) { described_class.new(agent) }
|
19
|
+
|
20
|
+
before do
|
21
|
+
allow(agent).to receive(:client).and_return(client)
|
22
|
+
allow(client).to receive(:mxid).and_return('@test_bot:example.com')
|
23
|
+
allow(bot).to receive(:room).and_return(room)
|
24
|
+
end
|
25
|
+
|
26
|
+
<% if commands.any? %>
|
27
|
+
<% commands.each do |command| %>
|
28
|
+
describe '#<%= command %>' do
|
29
|
+
it 'responds to the <%= command %> command' do
|
30
|
+
expect(room).to receive(:send_notice).twice
|
31
|
+
bot.send(:<%= command %>)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
<% end %>
|
36
|
+
<% else %>
|
37
|
+
describe '#hello' do
|
38
|
+
it 'responds with a greeting' do
|
39
|
+
expect(room).to receive(:send_notice).with('Hello there!')
|
40
|
+
bot.hello
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'greets by name when provided' do
|
44
|
+
expect(room).to receive(:send_notice).with('Hello, Alice!')
|
45
|
+
bot.hello('Alice')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
<% end %>
|
49
|
+
|
50
|
+
describe 'inter-agent communication' do
|
51
|
+
let(:other_bot) { instance_double(ActiveMatrix::Bot::MultiInstanceBase, agent_name: 'other_bot') }
|
52
|
+
|
53
|
+
it 'responds to ping messages' do
|
54
|
+
expect(bot).to receive(:send_to_agent).with('other_bot', hash_including(type: 'pong'))
|
55
|
+
bot.receive_message({ type: 'ping' }, from: other_bot)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'memory access' do
|
60
|
+
it 'has access to agent memory' do
|
61
|
+
expect(bot.memory).to be_a(ActiveMatrix::Memory::AgentMemory)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'has access to global memory' do
|
65
|
+
expect(bot.global_memory).to be_a(ActiveMatrix::Memory::GlobalMemory)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
module ActiveMatrix
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include ActiveRecord::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
12
|
+
|
13
|
+
desc 'Creates ActiveMatrix migrations and initializers'
|
14
|
+
|
15
|
+
def create_migrations
|
16
|
+
migration_template 'create_matrix_agents.rb', 'db/migrate/create_matrix_agents.rb'
|
17
|
+
migration_template 'create_agent_memories.rb', 'db/migrate/create_agent_memories.rb'
|
18
|
+
migration_template 'create_conversation_contexts.rb', 'db/migrate/create_conversation_contexts.rb'
|
19
|
+
migration_template 'create_global_memories.rb', 'db/migrate/create_global_memories.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_initializer
|
23
|
+
template 'active_matrix.rb', 'config/initializers/active_matrix.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_models
|
27
|
+
template 'matrix_agent.rb', 'app/models/matrix_agent.rb'
|
28
|
+
template 'agent_memory.rb', 'app/models/agent_memory.rb'
|
29
|
+
template 'conversation_context.rb', 'app/models/conversation_context.rb'
|
30
|
+
template 'global_memory.rb', 'app/models/global_memory.rb'
|
31
|
+
end
|
32
|
+
|
33
|
+
def display_post_install
|
34
|
+
readme 'README' if behavior == :invoke
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def migration_version
|
40
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
===============================================================================
|
2
|
+
|
3
|
+
ActiveMatrix Multi-Agent System has been installed!
|
4
|
+
|
5
|
+
Next steps:
|
6
|
+
|
7
|
+
1. Run migrations:
|
8
|
+
rails db:migrate
|
9
|
+
|
10
|
+
2. Create your first bot:
|
11
|
+
rails generate active_matrix:bot MyFirstBot
|
12
|
+
|
13
|
+
3. Configure your agents in the Rails console:
|
14
|
+
agent = MatrixAgent.create!(
|
15
|
+
name: 'captain',
|
16
|
+
homeserver: 'https://matrix.org',
|
17
|
+
username: 'captain_bot',
|
18
|
+
password: 'secure_password',
|
19
|
+
bot_class: 'MyFirstBot'
|
20
|
+
)
|
21
|
+
|
22
|
+
4. Start the agent system:
|
23
|
+
ActiveMatrix::AgentManager.instance.start_all
|
24
|
+
|
25
|
+
Or start individual agents:
|
26
|
+
ActiveMatrix::AgentManager.instance.start_agent(agent)
|
27
|
+
|
28
|
+
For more information, visit: https://github.com/seuros/agent_smith
|
29
|
+
|
30
|
+
===============================================================================
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ActiveMatrix configuration
|
4
|
+
ActiveMatrix.configure do |config|
|
5
|
+
# Agent configuration
|
6
|
+
config.agent_startup_delay = 2.seconds # Delay between starting each agent
|
7
|
+
config.max_agents_per_process = 10 # Maximum agents in a single process
|
8
|
+
config.agent_health_check_interval = 30.seconds
|
9
|
+
|
10
|
+
# Memory configuration
|
11
|
+
config.conversation_history_limit = 20
|
12
|
+
config.conversation_stale_after = 1.day
|
13
|
+
config.memory_cleanup_interval = 1.hour
|
14
|
+
|
15
|
+
# Event routing configuration
|
16
|
+
config.event_queue_size = 1000
|
17
|
+
config.event_processing_timeout = 30.seconds
|
18
|
+
|
19
|
+
# Client pool configuration
|
20
|
+
config.max_clients_per_homeserver = 5
|
21
|
+
config.client_idle_timeout = 5.minutes
|
22
|
+
|
23
|
+
# Logging
|
24
|
+
config.agent_log_level = :info
|
25
|
+
config.log_agent_events = Rails.env.development?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Start agent manager on Rails boot (can be disabled in environments)
|
29
|
+
if Rails.env.production? || ENV['START_AGENTS'].present?
|
30
|
+
Rails.application.config.after_initialize do
|
31
|
+
ActiveMatrix::AgentManager.instance.start_all
|
32
|
+
end
|
33
|
+
end
|