cirrocumulus 0.6.3 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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