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,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
|