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.
- data/README.rdoc +1 -22
- data/lib/cirrocumulus.rb +8 -2
- data/lib/cirrocumulus/{message.rb → agents/message.rb} +0 -0
- data/lib/cirrocumulus/channels.rb +129 -0
- data/lib/cirrocumulus/channels/jabber.rb +168 -0
- data/lib/cirrocumulus/environment.rb +46 -0
- data/lib/cirrocumulus/facts.rb +135 -0
- data/lib/cirrocumulus/identifier.rb +84 -0
- data/lib/cirrocumulus/ontology.rb +333 -49
- data/lib/cirrocumulus/pattern_matching.rb +175 -0
- data/lib/cirrocumulus/remote_console.rb +29 -0
- data/lib/cirrocumulus/rule_queue.rb +69 -0
- data/lib/cirrocumulus/rules/engine.rb +7 -5
- data/lib/cirrocumulus/rules/fact.rb +22 -0
- data/lib/cirrocumulus/rules/pattern_matcher.rb +19 -0
- data/lib/cirrocumulus/rules/run_queue.rb +1 -0
- data/lib/cirrocumulus/saga.rb +44 -25
- data/lib/console.rb +96 -0
- metadata +79 -72
- data/.document +0 -5
- data/.idea/.name +0 -1
- data/.idea/.rakeTasks +0 -7
- data/.idea/cirrocumulus.iml +0 -87
- data/.idea/encodings.xml +0 -5
- data/.idea/misc.xml +0 -5
- data/.idea/modules.xml +0 -9
- data/.idea/scopes/scope_settings.xml +0 -5
- data/.idea/vcs.xml +0 -7
- data/.idea/workspace.xml +0 -614
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -43
- data/Rakefile +0 -37
- data/VERSION +0 -1
- data/cirrocumulus.gemspec +0 -106
- data/lib/.gitignore +0 -5
- data/lib/cirrocumulus/agent_wrapper.rb +0 -67
- data/lib/cirrocumulus/jabber_bus.rb +0 -140
- data/lib/cirrocumulus/rule_engine.rb +0 -2
- data/lib/cirrocumulus/rule_server.rb +0 -49
- data/test/Gemfile +0 -3
- data/test/Gemfile.lock +0 -27
- data/test/helper.rb +0 -18
- data/test/test.rb +0 -85
- data/test/test2.rb +0 -30
- 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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
class RuleDescription
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :conditions
|
8
|
+
attr_reader :options
|
9
|
+
attr_reader :code
|
18
10
|
|
19
|
-
|
20
|
-
|
11
|
+
def initialize(name, conditions, options, code)
|
12
|
+
@name = name
|
13
|
+
@conditions = conditions
|
14
|
+
@options = options
|
15
|
+
@code = code
|
16
|
+
end
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
def ==(other)
|
19
|
+
name == other.name
|
20
|
+
end
|
21
|
+
end
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
23
|
+
class Ontology
|
24
|
+
class << self
|
25
|
+
@@inproc_agents = {}
|
26
|
+
@@loaded_rules = {}
|
27
|
+
@@ontology_names = {}
|
31
28
|
|
32
|
-
|
33
|
-
|
29
|
+
def register_ontology_instance(instance)
|
30
|
+
@@inproc_agents[instance.identifier] = instance
|
31
|
+
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
42
|
+
nil
|
47
43
|
end
|
48
44
|
|
49
|
-
|
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
|
60
|
+
def dump_sagas(identifier)
|
61
|
+
instance = query_ontology_instance(identifier)
|
62
|
+
return instance.nil? ? [] : instance.dump_sagas()
|
52
63
|
end
|
53
64
|
|
54
|
-
|
55
|
-
|
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
|
-
|
59
|
-
|
60
|
-
saga =
|
61
|
-
|
62
|
-
|
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
|
-
|
66
|
-
|
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
|