cirrocumulus 0.6.3 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,175 @@
|
|
1
|
+
class MatchResult
|
2
|
+
def initialize(rule)
|
3
|
+
@rule = rule
|
4
|
+
@matched_facts = []
|
5
|
+
@parameters = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :rule
|
9
|
+
attr_reader :matched_facts
|
10
|
+
attr_accessor :parameters
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
rule == other.rule && matched_facts == other.matched_facts
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class PatternMatcher
|
18
|
+
def initialize(facts)
|
19
|
+
@facts = facts
|
20
|
+
end
|
21
|
+
|
22
|
+
def match(pattern)
|
23
|
+
res = []
|
24
|
+
find_matches_for_condition(pattern).each do |fact|
|
25
|
+
res << bind_parameters(pattern, fact.data, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
res
|
29
|
+
end
|
30
|
+
|
31
|
+
def matches?(rule)
|
32
|
+
trace "Processing rule '#{rule.name}' (#{rule.conditions.size} condition(s)):"
|
33
|
+
|
34
|
+
pattern_candidates = []
|
35
|
+
rule.conditions.each do |pattern|
|
36
|
+
pattern_candidates << find_matches_for_condition(pattern)
|
37
|
+
end
|
38
|
+
|
39
|
+
return nil if !pattern_candidates.all? {|c| c.size > 0}
|
40
|
+
|
41
|
+
intersect_matches_for_each_condition(rule, pattern_candidates)
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_matches_for_condition(pattern)
|
45
|
+
trace "=> attempting to match pattern #{pattern.inspect}"
|
46
|
+
fact_matches = true
|
47
|
+
candidates = []
|
48
|
+
|
49
|
+
@facts.each do |fact|
|
50
|
+
next if fact.data.size != pattern.size
|
51
|
+
fact_matches = true
|
52
|
+
|
53
|
+
pattern.each_with_index do |el,i|
|
54
|
+
if el.is_a?(Symbol) && el.to_s.upcase == el.to_s # parameter
|
55
|
+
else
|
56
|
+
fact_matches = false if el != fact.data[i]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
candidates << fact if fact_matches
|
61
|
+
end
|
62
|
+
|
63
|
+
trace "=> candidates: #{candidates.size}" if candidates.size > 0
|
64
|
+
candidates
|
65
|
+
end
|
66
|
+
|
67
|
+
def intersect_matches_for_each_condition(rule, candidates)
|
68
|
+
result = []
|
69
|
+
attempt = []
|
70
|
+
while (attempt = generate_combination(rule, candidates, attempt)) != [] do
|
71
|
+
bindings = test_condition_parameters_combination(rule, candidates, attempt)
|
72
|
+
if bindings
|
73
|
+
match_data = MatchResult.new(rule)
|
74
|
+
attempt.each_with_index {|a,i| match_data.matched_facts << candidates[i][a]}
|
75
|
+
match_data.parameters = bindings
|
76
|
+
result << match_data
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_condition_parameters_combination(rule, candidates, attempt)
|
84
|
+
facts = []
|
85
|
+
attempt.each_with_index {|a,i| facts << candidates[i][a].data}
|
86
|
+
|
87
|
+
binded_params = {}
|
88
|
+
pattern_params = {}
|
89
|
+
facts.each_with_index do |fact,i|
|
90
|
+
pattern_params = bind_parameters(rule.conditions[i], fact, binded_params)
|
91
|
+
if pattern_params.nil? # failure, parameters mismatch
|
92
|
+
return nil
|
93
|
+
else
|
94
|
+
binded_params.merge!(pattern_params)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
binded_params
|
99
|
+
end
|
100
|
+
|
101
|
+
def bind_parameters(pattern, fact, current_bindings)
|
102
|
+
result = {}
|
103
|
+
|
104
|
+
pattern.each_with_index do |p,i|
|
105
|
+
if p.is_a?(Symbol) && p.to_s.upcase == p.to_s
|
106
|
+
return nil if current_bindings.has_key?(p) && current_bindings[p] != fact[i]
|
107
|
+
result[p] = fact[i]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_combination(rule, candidates, attempt)
|
115
|
+
next_attempt = []
|
116
|
+
|
117
|
+
if attempt == []
|
118
|
+
rule.conditions.each {|pattern| next_attempt << 0}
|
119
|
+
else
|
120
|
+
next_attempt = increment_attempt(attempt, rule.conditions.size - 1, candidates.map {|c| c.size})
|
121
|
+
end
|
122
|
+
|
123
|
+
next_attempt
|
124
|
+
end
|
125
|
+
|
126
|
+
def increment_attempt(attempt, idx, limits)
|
127
|
+
return [] if idx < 0
|
128
|
+
|
129
|
+
if attempt[idx] < limits[idx] - 1
|
130
|
+
attempt[idx] += 1
|
131
|
+
else
|
132
|
+
i = idx
|
133
|
+
while i < limits.size do
|
134
|
+
attempt[i] = 0
|
135
|
+
i += 1
|
136
|
+
end
|
137
|
+
|
138
|
+
return increment_attempt(attempt, idx-1, limits)
|
139
|
+
end
|
140
|
+
|
141
|
+
attempt
|
142
|
+
end
|
143
|
+
|
144
|
+
def pattern_matches?(fact, pattern, current_params = {})
|
145
|
+
return nil if fact.size != pattern.size
|
146
|
+
|
147
|
+
binded_params = {}
|
148
|
+
|
149
|
+
pattern.each_with_index do |el,i|
|
150
|
+
if el.is_a?(Symbol) && el.to_s.upcase == el.to_s
|
151
|
+
if current_params && current_params.has_key?(el)
|
152
|
+
current_value = current_params[el]
|
153
|
+
return nil if fact[i] != current_value
|
154
|
+
else
|
155
|
+
binded_params[el] = fact[i]
|
156
|
+
end
|
157
|
+
else
|
158
|
+
return nil if el != fact[i]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
binded_params
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def trace(msg)
|
168
|
+
puts "[TRACE] %s" % msg if false
|
169
|
+
end
|
170
|
+
|
171
|
+
def debug(msg)
|
172
|
+
puts "[DBG] %s" % msg if false
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'sexpistol'
|
3
|
+
require_relative 'identifier'
|
4
|
+
|
5
|
+
class RemoteConsole
|
6
|
+
def initialize
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
def list_inproc_agents
|
11
|
+
Ontology.list_ontology_instances()
|
12
|
+
end
|
13
|
+
|
14
|
+
def assert(instance, data)
|
15
|
+
Ontology.assert(LocalIdentifier.new(instance), Sexpistol.new.parse_string(data)[0])
|
16
|
+
end
|
17
|
+
|
18
|
+
def retract(instance, data)
|
19
|
+
Ontology.retract(LocalIdentifier.new(instance), Sexpistol.new.parse_string(data)[0])
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump_kb(instance)
|
23
|
+
Ontology.dump_kb(LocalIdentifier.new(instance))
|
24
|
+
end
|
25
|
+
|
26
|
+
def dump_sagas(instance)
|
27
|
+
Ontology.dump_sagas(LocalIdentifier.new(instance))
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class RuleQueue
|
4
|
+
class QueueEntry
|
5
|
+
attr_reader :run_data
|
6
|
+
attr_reader :rule
|
7
|
+
attr_reader :params
|
8
|
+
attr_reader :run_at
|
9
|
+
attr_accessor :state
|
10
|
+
|
11
|
+
def initialize(run_data, run_at = nil)
|
12
|
+
@run_data = run_data
|
13
|
+
@rule = run_data.rule
|
14
|
+
@params = run_data.parameters
|
15
|
+
@run_at = run_at
|
16
|
+
@state = :queued
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(ontology_instance)
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@queue = []
|
23
|
+
@ontology_instance = ontology_instance
|
24
|
+
end
|
25
|
+
|
26
|
+
def push(entry)
|
27
|
+
@mutex.synchronize do
|
28
|
+
@queue.push(QueueEntry.new(entry)) unless @queue.find {|e| e.state != :finished && e.run_data == entry}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def pop
|
33
|
+
@mutex.synchronize do
|
34
|
+
@queue.empty? ? nil : @queue.shift
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def size
|
39
|
+
@mutex.synchronize do
|
40
|
+
@queue.size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_queued_rules
|
45
|
+
while (entry = pop) != nil do
|
46
|
+
next if entry.state == :finished
|
47
|
+
if entry.run_at && (entry.run_at < Time.now)
|
48
|
+
push(entry)
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
if entry.run_data.matched_facts.all? {|fact| !fact.is_deleted}
|
53
|
+
begin
|
54
|
+
debug "Executing #{entry.rule.name}#{entry.params.inspect}"
|
55
|
+
entry.rule.code.call(@ontology_instance, entry.params)
|
56
|
+
entry.state
|
57
|
+
rescue Exception => e
|
58
|
+
puts "[WARN] Exception while executing rule: %s\n%s" % [e.to_s, e.backtrace.to_s]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def debug(msg)
|
67
|
+
puts msg
|
68
|
+
end
|
69
|
+
end
|
@@ -182,10 +182,12 @@ module RuleEngine
|
|
182
182
|
end
|
183
183
|
|
184
184
|
def tick()
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
185
|
+
@mutex.synchronize do
|
186
|
+
to_retract = []
|
187
|
+
@facts.each {|fact| to_retract << fact if fact.timed_out? }
|
188
|
+
to_retract.each {|fact| retract_nonblocking(fact.data, true) }
|
189
|
+
process() unless to_retract.empty?
|
190
|
+
end
|
189
191
|
rescue Exception => ex
|
190
192
|
p ex
|
191
193
|
end
|
@@ -386,7 +388,7 @@ module RuleEngine
|
|
386
388
|
def process()
|
387
389
|
self.class.current_ruleset.each do |rule|
|
388
390
|
binded_params = matches?(rule)
|
389
|
-
next if binded_params.
|
391
|
+
next if binded_params.nil? || binded_params.empty?
|
390
392
|
|
391
393
|
binded_params.each {|params| execute_rule(params)}
|
392
394
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cirrocumulus
|
2
|
+
module Ruler
|
3
|
+
# Fact.
|
4
|
+
# Piece of information, collected and operated in Cirrocumulus.
|
5
|
+
# We also remember the time, when this fact was created (observed). Optionally can have expiration time
|
6
|
+
class Fact
|
7
|
+
attr_reader :data
|
8
|
+
attr_accessor :is_deleted
|
9
|
+
|
10
|
+
def initialize(data, time, options)
|
11
|
+
@data = data
|
12
|
+
@time = time
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def expired?
|
17
|
+
return false if @options[:expires] == nil
|
18
|
+
@time + @options[:expires] < Time.now
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cirrocumulus
|
2
|
+
module Ruler
|
3
|
+
# The Matcher.
|
4
|
+
# Matches and compares facts with each other. Searches and bind variables.
|
5
|
+
class PatternMatcher
|
6
|
+
def match(pattern, fact)
|
7
|
+
return false if fact.data.size != pattern.size
|
8
|
+
fact_matches = true
|
9
|
+
|
10
|
+
pattern.each_with_index do |el,i|
|
11
|
+
next if el.is_a?(Symbol) && el.to_s.upcase == el.to_s
|
12
|
+
fact_matches = false if el != fact.data[i]
|
13
|
+
end
|
14
|
+
|
15
|
+
fact_matches
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/cirrocumulus/saga.rb
CHANGED
@@ -1,55 +1,74 @@
|
|
1
|
+
#
|
2
|
+
# Saga. Implements long-running workflows
|
3
|
+
#
|
1
4
|
class Saga
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
+
class << self
|
6
|
+
@@saga_names = {}
|
7
|
+
|
8
|
+
def saga(saga_name = nil)
|
9
|
+
return @@saga_names[name] if saga_name.nil?
|
10
|
+
@@saga_names[name] = saga_name
|
11
|
+
end
|
12
|
+
end
|
5
13
|
|
6
|
-
DEFAULT_TIMEOUT = 15
|
7
|
-
LONG_TIMEOUT = 60
|
8
|
-
|
9
14
|
STATE_ERROR = -1
|
10
15
|
STATE_START = 0
|
11
16
|
STATE_FINISHED = 255
|
12
17
|
|
18
|
+
attr_reader :id
|
19
|
+
|
13
20
|
def initialize(id, ontology)
|
14
21
|
@id = id
|
15
22
|
@ontology = ontology
|
16
|
-
@finished = false
|
17
|
-
@timeout_at = -1
|
18
23
|
@state = STATE_START
|
24
|
+
@started_at = Time.now
|
19
25
|
end
|
20
26
|
|
21
27
|
def is_finished?
|
22
|
-
@
|
28
|
+
@state == STATE_ERROR || @state == STATE_FINISHED
|
23
29
|
end
|
24
30
|
|
25
|
-
def
|
26
|
-
|
31
|
+
def handle_reply(sender, contents, options = {}); end
|
32
|
+
|
33
|
+
def dump_parameters
|
34
|
+
""
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"%s type=%s, started at %s, state=%d, params: %s" % [@id, @@saga_names[self.class.name], @started_at, @state, dump_parameters]
|
27
39
|
end
|
28
40
|
|
29
41
|
protected
|
30
42
|
|
31
|
-
|
32
|
-
|
43
|
+
#
|
44
|
+
# Inter-agent communications with context of this saga.
|
45
|
+
#
|
46
|
+
def inform(agent, proposition)
|
47
|
+
@ontology.inform agent, proposition, :conversation_id => self.id
|
33
48
|
end
|
34
49
|
|
35
|
-
def
|
36
|
-
|
37
|
-
@timeout_at = Time.now.to_i + secs
|
50
|
+
def request(agent, action)
|
51
|
+
@ontology.request agent, action, :conversation_id => self.id
|
38
52
|
end
|
39
|
-
|
40
|
-
def
|
41
|
-
|
42
|
-
|
53
|
+
|
54
|
+
def query(agent, expression)
|
55
|
+
@ontology.query agent, expression, :reply_with => self.id
|
56
|
+
end
|
57
|
+
|
58
|
+
def query_if(agent, proposition)
|
59
|
+
@ontology.query_if agent, proposition, :reply_with => self.id
|
43
60
|
end
|
44
|
-
|
61
|
+
|
45
62
|
def finish()
|
46
|
-
clear_timeout()
|
47
63
|
change_state(STATE_FINISHED)
|
48
|
-
@finished = true
|
49
|
-
Log4r::Logger['agent'].debug "[#{id}] finished"
|
50
64
|
end
|
51
|
-
|
65
|
+
|
52
66
|
def error()
|
53
67
|
change_state(STATE_ERROR)
|
54
68
|
end
|
69
|
+
|
70
|
+
def change_state(new_state)
|
71
|
+
@state = new_state
|
72
|
+
end
|
73
|
+
|
55
74
|
end
|