cirrocumulus 0.6.3 → 0.9.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 (45) hide show
  1. data/README.rdoc +1 -22
  2. data/lib/cirrocumulus.rb +8 -2
  3. data/lib/cirrocumulus/{message.rb → agents/message.rb} +0 -0
  4. data/lib/cirrocumulus/channels.rb +129 -0
  5. data/lib/cirrocumulus/channels/jabber.rb +168 -0
  6. data/lib/cirrocumulus/environment.rb +46 -0
  7. data/lib/cirrocumulus/facts.rb +135 -0
  8. data/lib/cirrocumulus/identifier.rb +84 -0
  9. data/lib/cirrocumulus/ontology.rb +333 -49
  10. data/lib/cirrocumulus/pattern_matching.rb +175 -0
  11. data/lib/cirrocumulus/remote_console.rb +29 -0
  12. data/lib/cirrocumulus/rule_queue.rb +69 -0
  13. data/lib/cirrocumulus/rules/engine.rb +7 -5
  14. data/lib/cirrocumulus/rules/fact.rb +22 -0
  15. data/lib/cirrocumulus/rules/pattern_matcher.rb +19 -0
  16. data/lib/cirrocumulus/rules/run_queue.rb +1 -0
  17. data/lib/cirrocumulus/saga.rb +44 -25
  18. data/lib/console.rb +96 -0
  19. metadata +79 -72
  20. data/.document +0 -5
  21. data/.idea/.name +0 -1
  22. data/.idea/.rakeTasks +0 -7
  23. data/.idea/cirrocumulus.iml +0 -87
  24. data/.idea/encodings.xml +0 -5
  25. data/.idea/misc.xml +0 -5
  26. data/.idea/modules.xml +0 -9
  27. data/.idea/scopes/scope_settings.xml +0 -5
  28. data/.idea/vcs.xml +0 -7
  29. data/.idea/workspace.xml +0 -614
  30. data/Gemfile +0 -18
  31. data/Gemfile.lock +0 -43
  32. data/Rakefile +0 -37
  33. data/VERSION +0 -1
  34. data/cirrocumulus.gemspec +0 -106
  35. data/lib/.gitignore +0 -5
  36. data/lib/cirrocumulus/agent_wrapper.rb +0 -67
  37. data/lib/cirrocumulus/jabber_bus.rb +0 -140
  38. data/lib/cirrocumulus/rule_engine.rb +0 -2
  39. data/lib/cirrocumulus/rule_server.rb +0 -49
  40. data/test/Gemfile +0 -3
  41. data/test/Gemfile.lock +0 -27
  42. data/test/helper.rb +0 -18
  43. data/test/test.rb +0 -85
  44. data/test/test2.rb +0 -30
  45. data/test/test_cirrocumulus.rb +0 -7
@@ -0,0 +1,84 @@
1
+ #
2
+ # Syntax sugar for agent identifiers.
3
+ #
4
+ class Agent
5
+ def self.local(instance_name)
6
+ LocalIdentifier.new(instance_name)
7
+ end
8
+
9
+ def self.network(ontology_name)
10
+ JabberIdentifier.new(ontology_name)
11
+ end
12
+
13
+ def self.all(ontology_name = nil)
14
+ if ontology_name == nil
15
+ Broadcast.new
16
+ else
17
+ Autodiscover.new(ontology_name)
18
+ end
19
+ end
20
+
21
+ def self.remote(agent_identifier)
22
+ RemoteIdentifier.new(agent_identifier)
23
+ end
24
+ end
25
+
26
+ class Broadcast
27
+ def to_s
28
+ "(broadcast)"
29
+ end
30
+ end
31
+
32
+ #
33
+ # Agent identifier for remote agents.
34
+ #
35
+ class RemoteIdentifier
36
+ def initialize(remote_instance_name)
37
+ @remote_instance_name = remote_instance_name
38
+ end
39
+
40
+ def ==(other)
41
+ return false if other.nil? || !other.is_a?(RemoteIdentifier)
42
+
43
+ to_s == other.to_s
44
+ end
45
+
46
+ def hash
47
+ to_s.hash
48
+ end
49
+
50
+ def eql?(other)
51
+ self == other
52
+ end
53
+
54
+ def to_s
55
+ @remote_instance_name
56
+ end
57
+ end
58
+
59
+ #
60
+ # Agent identifier for local agents.
61
+ #
62
+ class LocalIdentifier
63
+ def initialize(ontology_name)
64
+ @ontology_name = ontology_name
65
+ end
66
+
67
+ def ==(other)
68
+ return false if other.nil? || !other.is_a?(LocalIdentifier)
69
+
70
+ to_s == other.to_s
71
+ end
72
+
73
+ def hash
74
+ to_s.hash
75
+ end
76
+
77
+ def eql?(other)
78
+ self == other
79
+ end
80
+
81
+ def to_s
82
+ "local-%s" % @ontology_name
83
+ end
84
+ end
@@ -1,69 +1,353 @@
1
- module Ontology
2
- class Base
3
- attr_reader :name
4
- attr_reader :agent
5
- attr_reader :sagas
6
-
7
- def initialize(name, agent)
8
- @name = name
9
- @agent = agent
10
- @sagas = []
11
- @saga_idx = 0
12
- end
1
+ require_relative 'pattern_matching'
2
+ require_relative 'rule_queue'
3
+ require_relative 'saga'
13
4
 
14
- # Restores saved state. Called once at initialization
15
- def restore_state()
16
- puts "call to dummy Ontology::Base.restore_state()"
17
- end
5
+ class RuleDescription
6
+ attr_reader :name
7
+ attr_reader :conditions
8
+ attr_reader :options
9
+ attr_reader :code
18
10
 
19
- def tick()
20
- time = Time.now.to_i
11
+ def initialize(name, conditions, options, code)
12
+ @name = name
13
+ @conditions = conditions
14
+ @options = options
15
+ @code = code
16
+ end
21
17
 
22
- @sagas.each do |saga|
23
- next if saga.is_finished?
18
+ def ==(other)
19
+ name == other.name
20
+ end
21
+ end
24
22
 
25
- begin
26
- saga.handle(nil) if saga.timed_out?(time)
27
- rescue Exception => e
28
- Log4r::Logger['agent'].warn "Got exception while ticking saga: %s\n%s" % [e.to_s, e.backtrace.to_s]
29
- end
30
- end
23
+ class Ontology
24
+ class << self
25
+ @@inproc_agents = {}
26
+ @@loaded_rules = {}
27
+ @@ontology_names = {}
31
28
 
32
- handle_tick()
33
- end
29
+ def register_ontology_instance(instance)
30
+ @@inproc_agents[instance.identifier] = instance
31
+ end
34
32
 
35
- def handle_incoming_message(message, kb)
36
- was_processed = false
37
- @sagas.each do |saga|
38
- next if saga.is_finished?
33
+ def list_ontology_instances
34
+ @@inproc_agents.each_key.map {|key| key.to_s}
35
+ end
39
36
 
40
- if [message.in_reply_to, message.conversation_id].include?(saga.id)
41
- was_processed = true
42
- saga.handle(message)
43
- end
44
- end
37
+ def query_ontology_instance(identifier)
38
+ @@inproc_agents.each_key do |key|
39
+ return @@inproc_agents[key] if key == identifier
40
+ end
45
41
 
46
- handle_message(message, kb) if !was_processed
42
+ nil
47
43
  end
48
44
 
49
- protected
45
+ def assert(identifier, data)
46
+ instance = query_ontology_instance(identifier)
47
+ instance.assert(data) if instance
48
+ end
49
+
50
+ def retract(identifier, data)
51
+ instance = query_ontology_instance(identifier)
52
+ instance.retract(data) if instance
53
+ end
54
+
55
+ def dump_kb(identifier)
56
+ instance = query_ontology_instance(identifier)
57
+ return instance.nil? ? [] : instance.dump_kb()
58
+ end
50
59
 
51
- def handle_tick()
60
+ def dump_sagas(identifier)
61
+ instance = query_ontology_instance(identifier)
62
+ return instance.nil? ? [] : instance.dump_sagas()
52
63
  end
53
64
 
54
- def handle_message(message, kb)
55
- puts "call to dummy Ontology::Base.handle_message()"
65
+ def current_ruleset()
66
+ return @@loaded_rules[name] ||= []
67
+ end
68
+
69
+ def enable_console
70
+ proxy = RemoteConsole.new
71
+ DRb.start_service('druby://0.0.0.0:8112', proxy)
72
+ end
73
+
74
+ def ontology(ontology_name)
75
+ @@ontology_names[name] = ontology_name
76
+ end
77
+
78
+ def rule(name, predicate, options = {}, &block)
79
+ return if predicate.empty?
80
+ return if current_ruleset.count {|rule| rule.name == name} > 0
81
+
82
+ current_ruleset << RuleDescription.new(name, predicate, options, block)
83
+ end
84
+ end
85
+
86
+ attr_reader :identifier
87
+
88
+ #
89
+ # Infrastructure code
90
+ #
91
+ def initialize(identifier)
92
+ @identifier = identifier
93
+ @facts = FactsDatabase.new()
94
+ @classes = []
95
+ @last_saga_id = 0
96
+ @sagas = []
97
+
98
+ self.class.register_ontology_instance(self)
99
+ @mutex = Mutex.new
100
+ @rule_queue = RuleQueue.new(self)
101
+ end
102
+
103
+ def name
104
+ @@ontology_names[self.class.name]
105
+ end
106
+
107
+ def run()
108
+ self.running = true
109
+
110
+ @thread = Thread.new(self) do |ontology|
111
+ while self.running do
112
+ ontology.timeout_facts
113
+ @rule_queue.run_queued_rules
114
+ sleep 0.1
115
+ end
116
+ end
117
+ end
118
+
119
+ def join
120
+ self.running = false
121
+ @thread.join()
122
+ end
123
+
124
+ def add_knowledge_class(klass)
125
+ @classes << klass
126
+ end
127
+
128
+ def assert(fact, options = {})
129
+ @mutex.synchronize do
130
+ assert_nb(fact, options)
131
+ end
132
+ end
133
+
134
+ def retract(fact)
135
+ @mutex.synchronize do
136
+ retract_nb(fact)
137
+ end
138
+ end
139
+
140
+ def replace(pattern, values, options = {})
141
+ @mutex.synchronize do
142
+ matcher = PatternMatcher.new(@facts.enumerate())
143
+ data = matcher.match(pattern)
144
+
145
+ if data.empty?
146
+ new_fact = pattern.clone
147
+
148
+ pattern.each_with_index do |item,i|
149
+ if item.is_a?(Symbol) && item.to_s.upcase == item.to_s
150
+ new_fact[i] = values.is_a?(Hash) ? values[item] : values
151
+ end
152
+ end
153
+
154
+ assert_nb(new_fact, options, false)
155
+ else
156
+ data.each do |match_data|
157
+ old_fact = pattern.clone
158
+ new_fact = pattern.clone
159
+ pattern.each_with_index do |item,i|
160
+ if match_data.include? item
161
+ old_fact[i] = match_data[item]
162
+ new_fact[i] = values.is_a?(Hash) ? values[item] : values
163
+ end
164
+ end
165
+
166
+ facts_are_same = true
167
+ old_fact.each_with_index do |item, idx|
168
+ new_item = new_fact[idx]
169
+ facts_are_same = false if new_item != item
170
+ end
171
+
172
+ unless facts_are_same
173
+ debug "replace #{pattern.inspect} for #{values.inspect}"
174
+
175
+ retract_nb(old_fact, true)
176
+ assert_nb(new_fact, {}, false)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def dump_kb()
184
+ @facts.enumerate.map {|fact| fact.data.to_s}
185
+ end
186
+
187
+ def dump_sagas()
188
+ @sagas.map {|saga| saga.to_s}
189
+ end
190
+
191
+ #
192
+ # Inter-agent communications
193
+ #
194
+
195
+ def create_saga(saga_class)
196
+ @last_saga_id += 1
197
+ saga = saga_class.new(saga_class.saga + '-' + @last_saga_id.to_s, self)
198
+ @sagas << saga
199
+
200
+ saga
201
+ end
202
+
203
+ def reply(options)
204
+ if options.has_key?(:conversation_id)
205
+ {:conversation_id => options[:conversation_id]}
206
+ elsif options.has_key?(:reply_with)
207
+ {:in_reply_to => options[:reply_with]}
208
+ else
209
+ {}
56
210
  end
211
+ end
212
+
213
+ #
214
+ # Inform another agent about a fact. Normally, it will be added to it's KB.
215
+ #
216
+ def inform(agent, fact, options = {})
217
+ puts "%25s | inform %s about %s %s" % [identifier, agent, Sexpistol.new.to_sexp(fact), print_message_options(options)]
218
+
219
+ channel = ChannelFactory.retrieve(identifier, agent)
220
+ channel.inform(identifier, fact, options) if channel
221
+ end
222
+
223
+ def inform_and_wait(agent, fact, options = {})
224
+
225
+ end
226
+
227
+ #
228
+ # Send request to another agent.
229
+ #
230
+ def request(agent, contents, options = {})
231
+ puts "%25s | %s -> %s" % [identifier.to_s, Sexpistol.new.to_sexp(contents), agent.to_s]
232
+
233
+ channel = ChannelFactory.retrieve(identifier, agent)
234
+ channel.request(identifier, contents) if channel
235
+ end
236
+
237
+ def request_and_wait(agent, contents, options = {})
238
+
239
+ end
240
+
241
+ def query(agent, expression, options = {})
242
+ puts "%25s | query %s about %s %s" % [identifier, agent, Sexpistol.new.to_sexp(expression), print_message_options(options)]
243
+
244
+ channel = ChannelFactory.retrieve(identifier, agent)
245
+ channel.query(identifier, expression, options) if channel
246
+ end
247
+
248
+ def query_and_wait(agent, smth, options = {})
249
+ end
250
+
251
+ #
252
+ # Send 'query-if' to another agent. Normally, it will reply if the expression is true or false.
253
+ #
254
+ def query_if(agent, fact, options = {})
255
+ end
256
+
257
+ def query_if_and_wait(agent, fact, options = {})
258
+ end
259
+
260
+ #
261
+ # Custom code to restore previous state. Called at startup.
262
+ #
263
+ def restore_state; end
264
+
265
+ #
266
+ # Handles incoming fact. By default, just adds this fact to KB or redirects its processing to corresponding saga
267
+ #
268
+ def handle_inform(sender, proposition, options = {})
269
+ puts "%25s | received %s from %s %s" % [identifier, Sexpistol.new.to_sexp(proposition), sender, print_message_options(options)]
57
270
 
58
- def create_saga(saga_class)
59
- @saga_idx += 1
60
- saga = saga_class.new(saga_class.to_s + '-' + @saga_idx.to_s, self)
61
- @sagas << saga
62
- saga
271
+ if options.has_key?(:conversation_id) || options.has_key?(:in_reply_to)
272
+ saga_id = options[:conversation_id] || options[:in_reply_to]
273
+ saga = @sagas.find {|saga| saga.id == saga_id}
274
+ saga.handle_reply(sender, proposition, :action => :inform) if saga
275
+ else
276
+ assert proposition, :origin => sender
63
277
  end
278
+ end
279
+
280
+ #
281
+ # Abstract method to handle requests to this ontology.
282
+ #
283
+ def handle_request(sender, contents, options = {}); end
284
+
285
+ def handle_query(sender, expression, options = {})
286
+ puts "%25s | %s queries %s %s" % [identifier, sender, Sexpistol.new.to_sexp(expression), print_message_options(options)]
287
+ end
288
+
289
+ #
290
+ # Handles query-if to ontology. By default, it lookups the fact in KB and replies to the sender.
291
+ #
292
+ def handle_query_if(sender, proposition, options = {})
293
+ puts "%25s | %s queries if %s %s" % [identifier, sender, Sexpistol.new.to_sexp(proposition), print_message_options(options)]
294
+ end
295
+
296
+ protected
297
+
298
+ attr_reader :facts
299
+ attr_accessor :running
300
+
301
+ def assert_nb(fact, options = {}, silent = false)
302
+ silent = options unless options.is_a?(Hash)
303
+ options = {} unless options.is_a?(Hash)
304
+
305
+ debug("assert #{fact}")
306
+ @facts.add(fact, options)
307
+ process_rules() unless silent
308
+ end
64
309
 
65
- def logger
66
- Log4r::Logger['agent']
310
+ def retract_nb(fact, options = {}, silent = false)
311
+ silent = options unless options.is_a?(Hash)
312
+ options = {} unless options.is_a?(Hash)
313
+
314
+ debug("retract #{fact}")
315
+ @facts.remove(fact)
316
+ process_rules() unless silent
317
+ end
318
+
319
+ def timeout_facts()
320
+ @mutex.synchronize do
321
+ to_retract = []
322
+ @facts.enumerate().each {|fact| to_retract << fact if fact.timed_out? }
323
+ to_retract.each {|fact| retract_nb(fact.data, true) }
324
+ process_rules() unless to_retract.empty?
67
325
  end
326
+ end
327
+
328
+ def process_rules()
329
+ matcher = PatternMatcher.new(@facts.enumerate)
330
+
331
+ self.class.current_ruleset.each do |rule|
332
+ binded_params = matcher.matches?(rule)
333
+ next if binded_params.nil? || binded_params.empty?
334
+
335
+ binded_params.each {|params| execute_rule(params)}
336
+ end
337
+ end
338
+
339
+ def execute_rule(match_data)
340
+ @rule_queue.push(match_data)
341
+ end
342
+
343
+ def print_message_options(options = {})
344
+ return if options.empty?
345
+
346
+ "[%s]" % options.map {|k,v| "%s=%s" % [k, v]}.join(',')
68
347
  end
348
+
349
+ def debug(msg)
350
+ puts "[DBG] %s" % msg
351
+ end
352
+
69
353
  end