activematrix 0.0.0 → 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 (60) 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/{matrix_sdk → active_matrix}/api.rb +18 -22
  6. data/lib/{matrix_sdk → active_matrix}/bot/base.rb +42 -39
  7. data/lib/{matrix_sdk → active_matrix}/bot/main.rb +4 -5
  8. data/lib/active_matrix/bot/multi_instance_base.rb +189 -0
  9. data/lib/active_matrix/bot.rb +7 -0
  10. data/lib/{matrix_sdk → active_matrix}/client.rb +21 -34
  11. data/lib/active_matrix/client_pool.rb +194 -0
  12. data/lib/{matrix_sdk → active_matrix}/errors.rb +4 -4
  13. data/lib/active_matrix/event_router.rb +215 -0
  14. data/lib/active_matrix/logging.rb +56 -0
  15. data/lib/active_matrix/memory/agent_memory.rb +128 -0
  16. data/lib/active_matrix/memory/base.rb +101 -0
  17. data/lib/active_matrix/memory/conversation_memory.rb +161 -0
  18. data/lib/active_matrix/memory/global_memory.rb +153 -0
  19. data/lib/active_matrix/memory.rb +28 -0
  20. data/lib/{matrix_sdk → active_matrix}/mxid.rb +2 -2
  21. data/lib/{matrix_sdk → active_matrix}/protocols/as.rb +1 -1
  22. data/lib/{matrix_sdk → active_matrix}/protocols/cs.rb +6 -8
  23. data/lib/{matrix_sdk → active_matrix}/protocols/is.rb +1 -1
  24. data/lib/{matrix_sdk → active_matrix}/protocols/msc.rb +6 -8
  25. data/lib/{matrix_sdk → active_matrix}/protocols/ss.rb +2 -2
  26. data/lib/active_matrix/railtie.rb +18 -0
  27. data/lib/{matrix_sdk → active_matrix}/response.rb +2 -2
  28. data/lib/{matrix_sdk → active_matrix}/room.rb +148 -72
  29. data/lib/{matrix_sdk → active_matrix}/rooms/space.rb +3 -7
  30. data/lib/{matrix_sdk → active_matrix}/user.rb +23 -15
  31. data/lib/active_matrix/util/account_data_cache.rb +129 -0
  32. data/lib/active_matrix/util/cacheable.rb +73 -0
  33. data/lib/{matrix_sdk → active_matrix}/util/events.rb +8 -8
  34. data/lib/{matrix_sdk → active_matrix}/util/extensions.rb +6 -15
  35. data/lib/active_matrix/util/state_event_cache.rb +167 -0
  36. data/lib/{matrix_sdk → active_matrix}/util/uri.rb +4 -4
  37. data/lib/active_matrix/version.rb +5 -0
  38. data/lib/active_matrix.rb +81 -0
  39. data/lib/generators/active_matrix/bot/bot_generator.rb +38 -0
  40. data/lib/generators/active_matrix/bot/templates/bot.rb.erb +111 -0
  41. data/lib/generators/active_matrix/bot/templates/bot_spec.rb.erb +68 -0
  42. data/lib/generators/active_matrix/install/install_generator.rb +44 -0
  43. data/lib/generators/active_matrix/install/templates/README +30 -0
  44. data/lib/generators/active_matrix/install/templates/active_matrix.rb +33 -0
  45. data/lib/generators/active_matrix/install/templates/agent_memory.rb +47 -0
  46. data/lib/generators/active_matrix/install/templates/conversation_context.rb +72 -0
  47. data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +17 -0
  48. data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +21 -0
  49. data/lib/generators/active_matrix/install/templates/create_global_memories.rb +20 -0
  50. data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +26 -0
  51. data/lib/generators/active_matrix/install/templates/global_memory.rb +70 -0
  52. data/lib/generators/active_matrix/install/templates/matrix_agent.rb +127 -0
  53. metadata +168 -30
  54. data/lib/matrix_sdk/bot.rb +0 -4
  55. data/lib/matrix_sdk/util/account_data_cache.rb +0 -91
  56. data/lib/matrix_sdk/util/state_event_cache.rb +0 -92
  57. data/lib/matrix_sdk/util/tinycache.rb +0 -140
  58. data/lib/matrix_sdk/util/tinycache_adapter.rb +0 -87
  59. data/lib/matrix_sdk/version.rb +0 -5
  60. data/lib/matrix_sdk.rb +0 -75
@@ -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
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module MatrixSdk
3
+ module ActiveMatrix
4
4
  class EventHandlerArray < Hash
5
- include MatrixSdk::Logging
5
+ include ActiveMatrix::Logging
6
6
  attr_accessor :reraise_exceptions
7
7
 
8
- def initialize(*args)
8
+ def initialize(*)
9
9
  @reraise_exceptions = false
10
10
 
11
- super(*args)
11
+ super
12
12
  end
13
13
 
14
14
  def add_handler(filter = nil, id = nil, &block)
@@ -32,7 +32,7 @@ module MatrixSdk
32
32
  end
33
33
 
34
34
  class Event
35
- extend MatrixSdk::Extensions
35
+ extend ActiveMatrix::Extensions
36
36
 
37
37
  attr_writer :handled
38
38
 
@@ -57,7 +57,7 @@ module MatrixSdk
57
57
 
58
58
  def initialize(error, source)
59
59
  @error = error
60
- super source
60
+ super(source)
61
61
  end
62
62
 
63
63
  def source
@@ -74,7 +74,7 @@ module MatrixSdk
74
74
  def initialize(sender, event = nil, filter = nil)
75
75
  @event = event
76
76
  @filter = filter || @event[:type]
77
- super sender
77
+ super(sender)
78
78
  end
79
79
 
80
80
  def matches?(filter, filter_override = nil)
@@ -93,7 +93,7 @@ module MatrixSdk
93
93
  end
94
94
 
95
95
  def to_s
96
- "#{event[:type]}: #{event.reject { |k, _v| k == :type }.to_json}"
96
+ "#{event[:type]}: #{event.except(:type).to_json}"
97
97
  end
98
98
 
99
99
  def method_missing(method, *args)
@@ -8,7 +8,11 @@ unless Object.respond_to? :yield_self
8
8
  end
9
9
  end
10
10
 
11
- module MatrixSdk
11
+ # Time.current is provided by ActiveSupport
12
+
13
+ # Time duration helpers are provided by ActiveSupport
14
+
15
+ module ActiveMatrix
12
16
  module Extensions
13
17
  def events(*symbols)
14
18
  module_name = "#{name}Events"
@@ -21,7 +25,7 @@ module MatrixSdk
21
25
  name = sym.to_s
22
26
 
23
27
  initializers << "
24
- @on_#{name} = MatrixSdk::EventHandlerArray.new
28
+ @on_#{name} = ActiveMatrix::EventHandlerArray.new
25
29
  "
26
30
  readers << ":on_#{name}"
27
31
  methods << "
@@ -69,17 +73,4 @@ module MatrixSdk
69
73
  *, __FILE__, __LINE__ - 14
70
74
  end
71
75
  end
72
-
73
- module Logging
74
- def logger
75
- return MatrixSdk.logger if MatrixSdk.global_logger?
76
- return @logger if instance_variable_defined?(:@logger) && @logger
77
-
78
- ::Logging.logger[self]
79
- end
80
-
81
- def logger=(logger)
82
- @logger = logger
83
- end
84
- end
85
76
  end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix::Util
4
+ class StateEventCache
5
+ extend ActiveMatrix::Extensions
6
+ include Enumerable
7
+
8
+ attr_reader :room
9
+
10
+ attr_accessor :cache_time
11
+
12
+ ignore_inspect :client, :room
13
+
14
+ def initialize(room, cache_time: 30 * 60, **_params)
15
+ raise ArgumentError, 'Must be given a Room instance' unless room.is_a? ActiveMatrix::Room
16
+
17
+ @room = room
18
+ @cache_time = cache_time
19
+ end
20
+
21
+ def client
22
+ @room.client
23
+ end
24
+
25
+ def reload!
26
+ # Clear all cache entries for this room's state
27
+ cache.delete_matched("activematrix:room:#{room.id}:state:*") if cache_available?
28
+ end
29
+
30
+ def keys
31
+ # State is not enumerable when using Rails.cache
32
+ # This would require keeping track of keys separately
33
+ []
34
+ end
35
+
36
+ def values
37
+ []
38
+ end
39
+
40
+ def size
41
+ keys.count
42
+ end
43
+
44
+ def key?(type, key = nil)
45
+ cache_available? && cache.exist?(cache_key(type, key))
46
+ end
47
+
48
+ def expire(type, key = nil)
49
+ cache.delete(cache_key(type, key)) if cache_available?
50
+ end
51
+
52
+ def each(live: false)
53
+ to_enum(__method__, live: live) { 0 } unless block_given?
54
+ # Not enumerable with Rails.cache
55
+ end
56
+
57
+ def delete(type, key = nil)
58
+ type = type.to_s unless type.is_a? String
59
+ client.api.set_room_state(room.id, type, {}, **{ state_key: key }.compact)
60
+ cache.delete(cache_key(type, key)) if cache_available?
61
+ end
62
+
63
+ def [](type, key = nil)
64
+ type = type.to_s unless type.is_a? String
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)
99
+ end
100
+ end
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
+
108
+ def []=(type, key = nil, value) # rubocop:disable Style/OptionalArguments Not possible to put optional last
109
+ type = type.to_s unless type.is_a? String
110
+ client.api.set_room_state(room.id, type, value, **{ state_key: key }.compact)
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
165
+ end
166
+ end
167
+ end
@@ -25,8 +25,8 @@ module URI
25
25
  class MATRIX < Generic
26
26
  attr_reader :authority, :action, :mxid, :mxid2, :via
27
27
 
28
- def initialize(*args)
29
- super(*args)
28
+ def initialize(*)
29
+ super
30
30
 
31
31
  @action = nil
32
32
  @authority = nil
@@ -64,7 +64,7 @@ module URI
64
64
  component = components.shift
65
65
  raise InvalidComponentError, "component can't be empty" if component.nil? || component.empty?
66
66
 
67
- @mxid = MatrixSdk::MXID.new("#{sigil}#{component}")
67
+ @mxid = ActiveMatrix::MXID.new("#{sigil}#{component}")
68
68
 
69
69
  if components.size == 2
70
70
  sigil2 = case components.shift
@@ -76,7 +76,7 @@ module URI
76
76
  component = components.shift
77
77
  raise InvalidComponentError, "component can't be empty" if component.nil? || component.empty?
78
78
 
79
- @mxid2 = MatrixSdk::MXID.new("#{sigil2}#{component}")
79
+ @mxid2 = ActiveMatrix::MXID.new("#{sigil2}#{component}")
80
80
  end
81
81
 
82
82
  return unless @query
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMatrix
4
+ VERSION = '0.0.2'
5
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_matrix/version'
4
+ require_relative 'active_matrix/logging'
5
+ require_relative 'active_matrix/util/extensions'
6
+ require_relative 'active_matrix/util/uri'
7
+ require_relative 'active_matrix/util/events'
8
+ require_relative 'active_matrix/errors'
9
+
10
+ require 'json'
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'
17
+
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
+
56
+ # Set up Zeitwerk loader
57
+ Loader = Zeitwerk::Loader.for_gem
58
+
59
+ # Ignore directories that shouldn't be autoloaded
60
+ Loader.ignore("#{__dir__}/generators")
61
+
62
+ # Configure inflections for special cases
63
+ Loader.inflector.inflect(
64
+ 'mxid' => 'MXID',
65
+ 'uri' => 'URI',
66
+ 'as' => 'AS',
67
+ 'cs' => 'CS',
68
+ 'is' => 'IS',
69
+ 'ss' => 'SS',
70
+ 'msc' => 'MSC'
71
+ )
72
+
73
+ # Setup Zeitwerk autoloading
74
+ Loader.setup
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
+
79
+ # Load Railtie for Rails integration
80
+ require 'active_matrix/railtie' if defined?(Rails::Railtie)
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