kbs 0.0.1 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,1335 @@
|
|
|
1
|
+
# Multi-Agent Systems
|
|
2
|
+
|
|
3
|
+
Build collaborative multi-agent systems using KBS blackboard memory for coordination, message passing, and distributed reasoning.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
This example demonstrates a smart home automation system with:
|
|
8
|
+
|
|
9
|
+
- **Multiple Specialized Agents** - Temperature, security, energy, scheduling
|
|
10
|
+
- **Blackboard Coordination** - Shared persistent workspace
|
|
11
|
+
- **Message Passing** - Inter-agent communication via priority queues
|
|
12
|
+
- **Conflict Resolution** - Arbitration when agents disagree
|
|
13
|
+
- **Emergent Behavior** - Complex system behavior from simple agent rules
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────── Blackboard Memory ──────────────────┐
|
|
19
|
+
│ │
|
|
20
|
+
│ Facts: sensor_reading, alert, command, agent_status │
|
|
21
|
+
│ Messages: priority queues for agent communication │
|
|
22
|
+
│ Audit: complete history of all agent actions │
|
|
23
|
+
│ │
|
|
24
|
+
└──────────────────────┬──────────────────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
┌──────────────┼──────────────┬──────────────┐
|
|
27
|
+
│ │ │ │
|
|
28
|
+
┌─────▼────┐ ┌────▼─────┐ ┌────▼─────┐ ┌─────▼────┐
|
|
29
|
+
│ Temp │ │ Security │ │ Energy │ │ Schedule │
|
|
30
|
+
│ Agent │ │ Agent │ │ Agent │ │ Agent │
|
|
31
|
+
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Complete Implementation
|
|
35
|
+
|
|
36
|
+
### Multi-Agent Smart Home
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require 'kbs'
|
|
40
|
+
|
|
41
|
+
# Base agent class
|
|
42
|
+
class Agent
|
|
43
|
+
attr_reader :name, :engine
|
|
44
|
+
|
|
45
|
+
def initialize(name, engine)
|
|
46
|
+
@name = name
|
|
47
|
+
@engine = engine
|
|
48
|
+
@running = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def start
|
|
52
|
+
@running = true
|
|
53
|
+
@engine.add_fact(:agent_status, {
|
|
54
|
+
agent: @name,
|
|
55
|
+
status: "started",
|
|
56
|
+
timestamp: Time.now
|
|
57
|
+
})
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stop
|
|
61
|
+
@running = false
|
|
62
|
+
@engine.add_fact(:agent_status, {
|
|
63
|
+
agent: @name,
|
|
64
|
+
status: "stopped",
|
|
65
|
+
timestamp: Time.now
|
|
66
|
+
})
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def running?
|
|
70
|
+
@running
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def send_message(topic, content, priority: 50)
|
|
74
|
+
@engine.send_message(topic, {
|
|
75
|
+
from: @name,
|
|
76
|
+
content: content,
|
|
77
|
+
timestamp: Time.now
|
|
78
|
+
}, priority: priority)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def receive_messages(topic)
|
|
82
|
+
messages = []
|
|
83
|
+
while (msg = @engine.pop_message(topic))
|
|
84
|
+
messages << msg
|
|
85
|
+
end
|
|
86
|
+
messages
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Override in subclasses
|
|
90
|
+
def process
|
|
91
|
+
raise NotImplementedError, "Subclass must implement process"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_cycle
|
|
95
|
+
return unless running?
|
|
96
|
+
process
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Temperature control agent
|
|
101
|
+
class TemperatureAgent < Agent
|
|
102
|
+
def initialize(engine, target_temp: 22.0)
|
|
103
|
+
super("TemperatureAgent", engine)
|
|
104
|
+
@target_temp = target_temp
|
|
105
|
+
setup_rules
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def setup_rules
|
|
109
|
+
# Rule 1: Detect high temperature
|
|
110
|
+
high_temp_rule = KBS::Rule.new("detect_high_temperature", priority: 100) do |r|
|
|
111
|
+
r.conditions = [
|
|
112
|
+
KBS::Condition.new(:sensor_reading, {
|
|
113
|
+
type: "temperature",
|
|
114
|
+
location: :location?,
|
|
115
|
+
value: :temp?
|
|
116
|
+
}, predicate: lambda { |f| f[:value] > @target_temp + 2 }),
|
|
117
|
+
|
|
118
|
+
KBS::Condition.new(:temperature_alert, {
|
|
119
|
+
location: :location?
|
|
120
|
+
}, negated: true)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
r.action = lambda do |facts, bindings|
|
|
124
|
+
@engine.add_fact(:temperature_alert, {
|
|
125
|
+
location: bindings[:location?],
|
|
126
|
+
temperature: bindings[:temp?],
|
|
127
|
+
severity: "high",
|
|
128
|
+
timestamp: Time.now
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
send_message(:hvac_control, {
|
|
132
|
+
action: "cool",
|
|
133
|
+
location: bindings[:location?],
|
|
134
|
+
target: @target_temp
|
|
135
|
+
}, priority: 80)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Rule 2: Detect low temperature
|
|
140
|
+
low_temp_rule = KBS::Rule.new("detect_low_temperature", priority: 100) do |r|
|
|
141
|
+
r.conditions = [
|
|
142
|
+
KBS::Condition.new(:sensor_reading, {
|
|
143
|
+
type: "temperature",
|
|
144
|
+
location: :location?,
|
|
145
|
+
value: :temp?
|
|
146
|
+
}, predicate: lambda { |f| f[:value] < @target_temp - 2 }),
|
|
147
|
+
|
|
148
|
+
KBS::Condition.new(:temperature_alert, {
|
|
149
|
+
location: :location?
|
|
150
|
+
}, negated: true)
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
r.action = lambda do |facts, bindings|
|
|
154
|
+
@engine.add_fact(:temperature_alert, {
|
|
155
|
+
location: bindings[:location?],
|
|
156
|
+
temperature: bindings[:temp?],
|
|
157
|
+
severity: "low",
|
|
158
|
+
timestamp: Time.now
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
send_message(:hvac_control, {
|
|
162
|
+
action: "heat",
|
|
163
|
+
location: bindings[:location?],
|
|
164
|
+
target: @target_temp
|
|
165
|
+
}, priority: 80)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Rule 3: Clear alert when temperature normalized
|
|
170
|
+
clear_alert_rule = KBS::Rule.new("clear_temperature_alert", priority: 90) do |r|
|
|
171
|
+
r.conditions = [
|
|
172
|
+
KBS::Condition.new(:sensor_reading, {
|
|
173
|
+
type: "temperature",
|
|
174
|
+
location: :location?,
|
|
175
|
+
value: :temp?
|
|
176
|
+
}, predicate: lambda { |f|
|
|
177
|
+
(f[:value] - @target_temp).abs <= 1
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
KBS::Condition.new(:temperature_alert, {
|
|
181
|
+
location: :location?
|
|
182
|
+
})
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
r.action = lambda do |facts, bindings|
|
|
186
|
+
alert = facts.find { |f|
|
|
187
|
+
f.type == :temperature_alert && f[:location] == bindings[:location?]
|
|
188
|
+
}
|
|
189
|
+
@engine.remove_fact(alert) if alert
|
|
190
|
+
|
|
191
|
+
send_message(:hvac_control, {
|
|
192
|
+
action: "off",
|
|
193
|
+
location: bindings[:location?]
|
|
194
|
+
}, priority: 50)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@engine.add_rule(high_temp_rule)
|
|
199
|
+
@engine.add_rule(low_temp_rule)
|
|
200
|
+
@engine.add_rule(clear_alert_rule)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def process
|
|
204
|
+
@engine.run
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Security monitoring agent
|
|
209
|
+
class SecurityAgent < Agent
|
|
210
|
+
def initialize(engine)
|
|
211
|
+
super("SecurityAgent", engine)
|
|
212
|
+
setup_rules
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def setup_rules
|
|
216
|
+
# Rule 1: Detect intrusion
|
|
217
|
+
intrusion_rule = KBS::Rule.new("detect_intrusion", priority: 100) do |r|
|
|
218
|
+
r.conditions = [
|
|
219
|
+
KBS::Condition.new(:sensor_reading, {
|
|
220
|
+
type: "motion",
|
|
221
|
+
location: :location?,
|
|
222
|
+
detected: true
|
|
223
|
+
}),
|
|
224
|
+
|
|
225
|
+
KBS::Condition.new(:occupancy, {
|
|
226
|
+
status: "away"
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
KBS::Condition.new(:security_alert, {
|
|
230
|
+
type: "intrusion",
|
|
231
|
+
location: :location?
|
|
232
|
+
}, negated: true)
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
r.action = lambda do |facts, bindings|
|
|
236
|
+
@engine.add_fact(:security_alert, {
|
|
237
|
+
type: "intrusion",
|
|
238
|
+
location: bindings[:location?],
|
|
239
|
+
severity: "critical",
|
|
240
|
+
timestamp: Time.now
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
send_message(:security_system, {
|
|
244
|
+
action: "alarm",
|
|
245
|
+
location: bindings[:location?]
|
|
246
|
+
}, priority: 100)
|
|
247
|
+
|
|
248
|
+
send_message(:notifications, {
|
|
249
|
+
type: "security",
|
|
250
|
+
message: "Intrusion detected at #{bindings[:location?]}"
|
|
251
|
+
}, priority: 100)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Rule 2: Door left open
|
|
256
|
+
door_open_rule = KBS::Rule.new("detect_door_open", priority: 90) do |r|
|
|
257
|
+
r.conditions = [
|
|
258
|
+
KBS::Condition.new(:sensor_reading, {
|
|
259
|
+
type: "door",
|
|
260
|
+
location: :location?,
|
|
261
|
+
state: "open",
|
|
262
|
+
timestamp: :time?
|
|
263
|
+
}, predicate: lambda { |f|
|
|
264
|
+
(Time.now - f[:timestamp]) > 300 # Open for 5 minutes
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
KBS::Condition.new(:security_alert, {
|
|
268
|
+
type: "door_open",
|
|
269
|
+
location: :location?
|
|
270
|
+
}, negated: true)
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
r.action = lambda do |facts, bindings|
|
|
274
|
+
@engine.add_fact(:security_alert, {
|
|
275
|
+
type: "door_open",
|
|
276
|
+
location: bindings[:location?],
|
|
277
|
+
severity: "medium",
|
|
278
|
+
timestamp: Time.now
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
send_message(:notifications, {
|
|
282
|
+
type: "security",
|
|
283
|
+
message: "Door at #{bindings[:location?]} left open"
|
|
284
|
+
}, priority: 70)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
@engine.add_rule(intrusion_rule)
|
|
289
|
+
@engine.add_rule(door_open_rule)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def process
|
|
293
|
+
@engine.run
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Energy management agent
|
|
298
|
+
class EnergyAgent < Agent
|
|
299
|
+
def initialize(engine, max_usage: 5000)
|
|
300
|
+
super("EnergyAgent", engine)
|
|
301
|
+
@max_usage = max_usage # watts
|
|
302
|
+
setup_rules
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def setup_rules
|
|
306
|
+
# Rule 1: High energy consumption
|
|
307
|
+
high_consumption_rule = KBS::Rule.new("detect_high_consumption", priority: 80) do |r|
|
|
308
|
+
r.conditions = [
|
|
309
|
+
KBS::Condition.new(:sensor_reading, {
|
|
310
|
+
type: "power",
|
|
311
|
+
value: :usage?
|
|
312
|
+
}, predicate: lambda { |f| f[:value] > @max_usage }),
|
|
313
|
+
|
|
314
|
+
KBS::Condition.new(:energy_alert, {
|
|
315
|
+
type: "high_consumption"
|
|
316
|
+
}, negated: true)
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
r.action = lambda do |facts, bindings|
|
|
320
|
+
@engine.add_fact(:energy_alert, {
|
|
321
|
+
type: "high_consumption",
|
|
322
|
+
usage: bindings[:usage?],
|
|
323
|
+
limit: @max_usage,
|
|
324
|
+
timestamp: Time.now
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
# Request non-essential devices to reduce consumption
|
|
328
|
+
send_message(:device_control, {
|
|
329
|
+
action: "reduce_consumption",
|
|
330
|
+
priority_level: "low"
|
|
331
|
+
}, priority: 60)
|
|
332
|
+
|
|
333
|
+
send_message(:notifications, {
|
|
334
|
+
type: "energy",
|
|
335
|
+
message: "High energy usage: #{bindings[:usage?]}W (limit: #{@max_usage}W)"
|
|
336
|
+
}, priority: 60)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Rule 2: Coordinate with HVAC during high usage
|
|
341
|
+
hvac_coordination_rule = KBS::Rule.new("coordinate_hvac_energy", priority: 75) do |r|
|
|
342
|
+
r.conditions = [
|
|
343
|
+
KBS::Condition.new(:energy_alert, {
|
|
344
|
+
type: "high_consumption"
|
|
345
|
+
}),
|
|
346
|
+
|
|
347
|
+
KBS::Condition.new(:temperature_alert, {
|
|
348
|
+
location: :location?
|
|
349
|
+
})
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
r.action = lambda do |facts, bindings|
|
|
353
|
+
# Ask temperature agent to reduce HVAC intensity
|
|
354
|
+
send_message(:hvac_control, {
|
|
355
|
+
action: "eco_mode",
|
|
356
|
+
location: bindings[:location?]
|
|
357
|
+
}, priority: 70)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
@engine.add_rule(high_consumption_rule)
|
|
362
|
+
@engine.add_rule(hvac_coordination_rule)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def process
|
|
366
|
+
@engine.run
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Scheduling agent
|
|
371
|
+
class ScheduleAgent < Agent
|
|
372
|
+
def initialize(engine)
|
|
373
|
+
super("ScheduleAgent", engine)
|
|
374
|
+
setup_rules
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def setup_rules
|
|
378
|
+
# Rule 1: Morning routine
|
|
379
|
+
morning_rule = KBS::Rule.new("morning_routine", priority: 70) do |r|
|
|
380
|
+
r.conditions = [
|
|
381
|
+
KBS::Condition.new(:time_event, {
|
|
382
|
+
event: "morning",
|
|
383
|
+
hour: :hour?
|
|
384
|
+
}),
|
|
385
|
+
|
|
386
|
+
KBS::Condition.new(:routine_executed, {
|
|
387
|
+
type: "morning"
|
|
388
|
+
}, negated: true)
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
r.action = lambda do |facts, bindings|
|
|
392
|
+
@engine.add_fact(:routine_executed, {
|
|
393
|
+
type: "morning",
|
|
394
|
+
timestamp: Time.now
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
# Update occupancy
|
|
398
|
+
@engine.add_fact(:occupancy, { status: "home" })
|
|
399
|
+
|
|
400
|
+
# Adjust temperature
|
|
401
|
+
send_message(:temperature_control, {
|
|
402
|
+
action: "set_target",
|
|
403
|
+
temperature: 22
|
|
404
|
+
}, priority: 50)
|
|
405
|
+
|
|
406
|
+
# Turn on lights
|
|
407
|
+
send_message(:device_control, {
|
|
408
|
+
action: "lights_on",
|
|
409
|
+
locations: ["bedroom", "kitchen"]
|
|
410
|
+
}, priority: 40)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Rule 2: Night routine
|
|
415
|
+
night_rule = KBS::Rule.new("night_routine", priority: 70) do |r|
|
|
416
|
+
r.conditions = [
|
|
417
|
+
KBS::Condition.new(:time_event, {
|
|
418
|
+
event: "night",
|
|
419
|
+
hour: :hour?
|
|
420
|
+
}),
|
|
421
|
+
|
|
422
|
+
KBS::Condition.new(:routine_executed, {
|
|
423
|
+
type: "night"
|
|
424
|
+
}, negated: true)
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
r.action = lambda do |facts, bindings|
|
|
428
|
+
@engine.add_fact(:routine_executed, {
|
|
429
|
+
type: "night",
|
|
430
|
+
timestamp: Time.now
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
# Update occupancy
|
|
434
|
+
@engine.add_fact(:occupancy, { status: "sleeping" })
|
|
435
|
+
|
|
436
|
+
# Lower temperature
|
|
437
|
+
send_message(:temperature_control, {
|
|
438
|
+
action: "set_target",
|
|
439
|
+
temperature: 18
|
|
440
|
+
}, priority: 50)
|
|
441
|
+
|
|
442
|
+
# Turn off lights except nightlights
|
|
443
|
+
send_message(:device_control, {
|
|
444
|
+
action: "lights_off",
|
|
445
|
+
exclude: ["bathroom_nightlight"]
|
|
446
|
+
}, priority: 40)
|
|
447
|
+
|
|
448
|
+
# Enable security
|
|
449
|
+
send_message(:security_system, {
|
|
450
|
+
action: "arm_night_mode"
|
|
451
|
+
}, priority: 60)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Rule 3: Away mode
|
|
456
|
+
away_rule = KBS::Rule.new("away_mode", priority: 70) do |r|
|
|
457
|
+
r.conditions = [
|
|
458
|
+
KBS::Condition.new(:time_event, {
|
|
459
|
+
event: "departure"
|
|
460
|
+
}),
|
|
461
|
+
|
|
462
|
+
KBS::Condition.new(:occupancy, {
|
|
463
|
+
status: "away"
|
|
464
|
+
}, negated: true)
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
r.action = lambda do |facts, bindings|
|
|
468
|
+
old_occupancy = @engine.facts.find { |f| f.type == :occupancy }
|
|
469
|
+
@engine.remove_fact(old_occupancy) if old_occupancy
|
|
470
|
+
|
|
471
|
+
@engine.add_fact(:occupancy, { status: "away" })
|
|
472
|
+
|
|
473
|
+
# Turn off all lights
|
|
474
|
+
send_message(:device_control, {
|
|
475
|
+
action: "all_lights_off"
|
|
476
|
+
}, priority: 40)
|
|
477
|
+
|
|
478
|
+
# Energy saving mode
|
|
479
|
+
send_message(:temperature_control, {
|
|
480
|
+
action: "eco_mode"
|
|
481
|
+
}, priority: 50)
|
|
482
|
+
|
|
483
|
+
# Arm security
|
|
484
|
+
send_message(:security_system, {
|
|
485
|
+
action: "arm_away_mode"
|
|
486
|
+
}, priority: 80)
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
@engine.add_rule(morning_rule)
|
|
491
|
+
@engine.add_rule(night_rule)
|
|
492
|
+
@engine.add_rule(away_rule)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def process
|
|
496
|
+
@engine.run
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Arbitration agent - resolves conflicts
|
|
501
|
+
class ArbitrationAgent < Agent
|
|
502
|
+
def initialize(engine)
|
|
503
|
+
super("ArbitrationAgent", engine)
|
|
504
|
+
setup_rules
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def setup_rules
|
|
508
|
+
# Rule: Security overrides energy savings
|
|
509
|
+
security_priority_rule = KBS::Rule.new("security_overrides_energy", priority: 95) do |r|
|
|
510
|
+
r.conditions = [
|
|
511
|
+
KBS::Condition.new(:security_alert, {
|
|
512
|
+
severity: "critical"
|
|
513
|
+
}),
|
|
514
|
+
|
|
515
|
+
KBS::Condition.new(:energy_alert, {
|
|
516
|
+
type: "high_consumption"
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
r.action = lambda do |facts, bindings|
|
|
521
|
+
# Cancel energy reduction requests
|
|
522
|
+
send_message(:device_control, {
|
|
523
|
+
action: "cancel_energy_reduction",
|
|
524
|
+
reason: "security_priority"
|
|
525
|
+
}, priority: 95)
|
|
526
|
+
|
|
527
|
+
# Notify
|
|
528
|
+
send_message(:notifications, {
|
|
529
|
+
type: "system",
|
|
530
|
+
message: "Security takes priority over energy savings"
|
|
531
|
+
}, priority: 90)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Rule: Comfort overrides energy during occupied hours
|
|
536
|
+
comfort_priority_rule = KBS::Rule.new("comfort_during_occupied", priority: 85) do |r|
|
|
537
|
+
r.conditions = [
|
|
538
|
+
KBS::Condition.new(:occupancy, {
|
|
539
|
+
status: :status?
|
|
540
|
+
}, predicate: lambda { |f| f[:status] == "home" || f[:status] == "sleeping" }),
|
|
541
|
+
|
|
542
|
+
KBS::Condition.new(:temperature_alert, {
|
|
543
|
+
severity: :severity?
|
|
544
|
+
}),
|
|
545
|
+
|
|
546
|
+
KBS::Condition.new(:energy_alert, {
|
|
547
|
+
type: "high_consumption"
|
|
548
|
+
})
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
r.action = lambda do |facts, bindings|
|
|
552
|
+
# Allow HVAC to continue despite high energy
|
|
553
|
+
send_message(:hvac_control, {
|
|
554
|
+
action: "maintain_comfort",
|
|
555
|
+
reason: "occupancy_priority"
|
|
556
|
+
}, priority: 85)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
@engine.add_rule(security_priority_rule)
|
|
561
|
+
@engine.add_rule(comfort_priority_rule)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def process
|
|
565
|
+
@engine.run
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Multi-agent system coordinator
|
|
570
|
+
class SmartHomeSystem
|
|
571
|
+
attr_reader :engine, :agents
|
|
572
|
+
|
|
573
|
+
def initialize(db_path: 'smart_home.db')
|
|
574
|
+
@engine = KBS::Blackboard::Engine.new(db_path: db_path)
|
|
575
|
+
@agents = []
|
|
576
|
+
@running = false
|
|
577
|
+
|
|
578
|
+
setup_agents
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def setup_agents
|
|
582
|
+
@agents << TemperatureAgent.new(@engine, target_temp: 22.0)
|
|
583
|
+
@agents << SecurityAgent.new(@engine)
|
|
584
|
+
@agents << EnergyAgent.new(@engine, max_usage: 5000)
|
|
585
|
+
@agents << ScheduleAgent.new(@engine)
|
|
586
|
+
@agents << ArbitrationAgent.new(@engine)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def start
|
|
590
|
+
@running = true
|
|
591
|
+
@agents.each(&:start)
|
|
592
|
+
|
|
593
|
+
puts "Smart Home System started with #{@agents.size} agents"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def stop
|
|
597
|
+
@running = false
|
|
598
|
+
@agents.each(&:stop)
|
|
599
|
+
|
|
600
|
+
@engine.close
|
|
601
|
+
puts "Smart Home System stopped"
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def add_sensor_reading(type, attributes)
|
|
605
|
+
@engine.add_fact(:sensor_reading, {
|
|
606
|
+
type: type,
|
|
607
|
+
timestamp: Time.now,
|
|
608
|
+
**attributes
|
|
609
|
+
})
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def trigger_time_event(event, attributes = {})
|
|
613
|
+
@engine.add_fact(:time_event, {
|
|
614
|
+
event: event,
|
|
615
|
+
timestamp: Time.now,
|
|
616
|
+
**attributes
|
|
617
|
+
})
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def run_cycle
|
|
621
|
+
# Each agent processes in sequence
|
|
622
|
+
@agents.each(&:run_cycle)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def run_continuous(interval: 1)
|
|
626
|
+
while @running
|
|
627
|
+
run_cycle
|
|
628
|
+
sleep interval
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def status
|
|
633
|
+
{
|
|
634
|
+
running: @running,
|
|
635
|
+
agents: @agents.map { |a| { name: a.name, running: a.running? } },
|
|
636
|
+
facts: @engine.facts.size,
|
|
637
|
+
alerts: @engine.facts.select { |f| f.type.to_s.include?("alert") }.size
|
|
638
|
+
}
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Usage Example 1: Temperature Control
|
|
643
|
+
puts "=== Example 1: Temperature Control ==="
|
|
644
|
+
system = SmartHomeSystem.new(db_path: ':memory:')
|
|
645
|
+
system.start
|
|
646
|
+
|
|
647
|
+
# Add temperature reading
|
|
648
|
+
system.add_sensor_reading("temperature", {
|
|
649
|
+
location: "living_room",
|
|
650
|
+
value: 26.0 # Above target (22°C)
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
# Run agent cycle
|
|
654
|
+
system.run_cycle
|
|
655
|
+
|
|
656
|
+
# Check for temperature alerts
|
|
657
|
+
alerts = system.engine.facts.select { |f| f.type == :temperature_alert }
|
|
658
|
+
puts "\nTemperature Alerts:"
|
|
659
|
+
alerts.each do |alert|
|
|
660
|
+
puts " Location: #{alert[:location]}"
|
|
661
|
+
puts " Temperature: #{alert[:temperature]}°C"
|
|
662
|
+
puts " Severity: #{alert[:severity]}"
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Check HVAC messages
|
|
666
|
+
hvac_messages = []
|
|
667
|
+
while (msg = system.engine.pop_message(:hvac_control))
|
|
668
|
+
hvac_messages << msg
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
puts "\nHVAC Control Messages:"
|
|
672
|
+
hvac_messages.each do |msg|
|
|
673
|
+
puts " From: #{msg[:content][:from]}"
|
|
674
|
+
puts " Action: #{msg[:content][:content][:action]}"
|
|
675
|
+
puts " Priority: #{msg[:priority]}"
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Usage Example 2: Security Event
|
|
679
|
+
puts "\n\n=== Example 2: Security Event ==="
|
|
680
|
+
system2 = SmartHomeSystem.new(db_path: ':memory:')
|
|
681
|
+
system2.start
|
|
682
|
+
|
|
683
|
+
# Set away mode
|
|
684
|
+
system2.engine.add_fact(:occupancy, { status: "away" })
|
|
685
|
+
|
|
686
|
+
# Motion detected while away
|
|
687
|
+
system2.add_sensor_reading("motion", {
|
|
688
|
+
location: "living_room",
|
|
689
|
+
detected: true
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
system2.run_cycle
|
|
693
|
+
|
|
694
|
+
# Check security alerts
|
|
695
|
+
security_alerts = system2.engine.facts.select { |f| f.type == :security_alert }
|
|
696
|
+
puts "\nSecurity Alerts:"
|
|
697
|
+
security_alerts.each do |alert|
|
|
698
|
+
puts " Type: #{alert[:type]}"
|
|
699
|
+
puts " Location: #{alert[:location]}"
|
|
700
|
+
puts " Severity: #{alert[:severity]}"
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Check notifications
|
|
704
|
+
notifications = []
|
|
705
|
+
while (msg = system2.engine.pop_message(:notifications))
|
|
706
|
+
notifications << msg
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
puts "\nNotifications:"
|
|
710
|
+
notifications.each do |msg|
|
|
711
|
+
puts " Type: #{msg[:content][:content][:type]}"
|
|
712
|
+
puts " Message: #{msg[:content][:content][:message]}"
|
|
713
|
+
puts " Priority: #{msg[:priority]}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Usage Example 3: Energy Management with Arbitration
|
|
717
|
+
puts "\n\n=== Example 3: Energy Management ==="
|
|
718
|
+
system3 = SmartHomeSystem.new(db_path: ':memory:')
|
|
719
|
+
system3.start
|
|
720
|
+
|
|
721
|
+
# High energy consumption
|
|
722
|
+
system3.add_sensor_reading("power", { value: 6000 }) # Above 5000W limit
|
|
723
|
+
|
|
724
|
+
# Also high temperature (competing concern)
|
|
725
|
+
system3.add_sensor_reading("temperature", {
|
|
726
|
+
location: "bedroom",
|
|
727
|
+
value: 26.0
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
# Home occupancy
|
|
731
|
+
system3.engine.add_fact(:occupancy, { status: "home" })
|
|
732
|
+
|
|
733
|
+
system3.run_cycle
|
|
734
|
+
|
|
735
|
+
# Check arbitration
|
|
736
|
+
energy_alerts = system3.engine.facts.select { |f| f.type == :energy_alert }
|
|
737
|
+
temp_alerts = system3.engine.facts.select { |f| f.type == :temperature_alert }
|
|
738
|
+
|
|
739
|
+
puts "\nEnergy Alerts: #{energy_alerts.size}"
|
|
740
|
+
puts "Temperature Alerts: #{temp_alerts.size}"
|
|
741
|
+
|
|
742
|
+
# HVAC should maintain comfort despite high energy (arbitration)
|
|
743
|
+
hvac_msgs = []
|
|
744
|
+
while (msg = system3.engine.pop_message(:hvac_control))
|
|
745
|
+
hvac_msgs << msg
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
puts "\nHVAC Messages:"
|
|
749
|
+
hvac_msgs.each do |msg|
|
|
750
|
+
puts " Action: #{msg[:content][:content][:action]}"
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# Usage Example 4: Morning Routine
|
|
754
|
+
puts "\n\n=== Example 4: Morning Routine ==="
|
|
755
|
+
system4 = SmartHomeSystem.new(db_path: ':memory:')
|
|
756
|
+
system4.start
|
|
757
|
+
|
|
758
|
+
# Trigger morning event
|
|
759
|
+
system4.trigger_time_event("morning", { hour: 7 })
|
|
760
|
+
|
|
761
|
+
system4.run_cycle
|
|
762
|
+
|
|
763
|
+
# Check messages sent to various subsystems
|
|
764
|
+
temp_msgs = []
|
|
765
|
+
while (msg = system4.engine.pop_message(:temperature_control))
|
|
766
|
+
temp_msgs << msg
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
device_msgs = []
|
|
770
|
+
while (msg = system4.engine.pop_message(:device_control))
|
|
771
|
+
device_msgs << msg
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
puts "\nMorning Routine Executed:"
|
|
775
|
+
puts " Temperature control messages: #{temp_msgs.size}"
|
|
776
|
+
puts " Device control messages: #{device_msgs.size}"
|
|
777
|
+
|
|
778
|
+
temp_msgs.each do |msg|
|
|
779
|
+
puts " - Set temperature to #{msg[:content][:content][:temperature]}°C"
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
device_msgs.each do |msg|
|
|
783
|
+
puts " - #{msg[:content][:content][:action]}"
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
puts "\nSystem Status:"
|
|
787
|
+
puts system4.status.inspect
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
## Key Features
|
|
791
|
+
|
|
792
|
+
### 1. Agent Autonomy
|
|
793
|
+
|
|
794
|
+
Each agent operates independently:
|
|
795
|
+
|
|
796
|
+
```ruby
|
|
797
|
+
class Agent
|
|
798
|
+
def run_cycle
|
|
799
|
+
return unless running?
|
|
800
|
+
process # Agent-specific logic
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### 2. Blackboard Coordination
|
|
806
|
+
|
|
807
|
+
Shared workspace for collaboration:
|
|
808
|
+
|
|
809
|
+
```ruby
|
|
810
|
+
# Agent 1 writes fact
|
|
811
|
+
@engine.add_fact(:temperature_alert, { location: "bedroom" })
|
|
812
|
+
|
|
813
|
+
# Agent 2 reads fact and responds
|
|
814
|
+
temperature_alert = @engine.facts.find { |f|
|
|
815
|
+
f.type == :temperature_alert && f[:location] == "bedroom"
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### 3. Message Passing
|
|
820
|
+
|
|
821
|
+
Priority-based communication:
|
|
822
|
+
|
|
823
|
+
```ruby
|
|
824
|
+
# Temperature agent sends message
|
|
825
|
+
send_message(:hvac_control, {
|
|
826
|
+
action: "cool",
|
|
827
|
+
location: "bedroom"
|
|
828
|
+
}, priority: 80)
|
|
829
|
+
|
|
830
|
+
# HVAC controller receives message
|
|
831
|
+
msg = @engine.pop_message(:hvac_control)
|
|
832
|
+
# Process highest priority message first
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### 4. Conflict Resolution
|
|
836
|
+
|
|
837
|
+
Arbitration agent resolves competing goals:
|
|
838
|
+
|
|
839
|
+
```ruby
|
|
840
|
+
# Security overrides energy savings
|
|
841
|
+
KBS::Rule.new("security_overrides_energy") do |r|
|
|
842
|
+
r.conditions = [
|
|
843
|
+
KBS::Condition.new(:security_alert, { severity: "critical" }),
|
|
844
|
+
KBS::Condition.new(:energy_alert, { type: "high_consumption" })
|
|
845
|
+
]
|
|
846
|
+
|
|
847
|
+
r.action = lambda do |facts, bindings|
|
|
848
|
+
# Cancel energy reduction
|
|
849
|
+
send_message(:device_control, {
|
|
850
|
+
action: "cancel_energy_reduction",
|
|
851
|
+
reason: "security_priority"
|
|
852
|
+
}, priority: 95)
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### 5. Emergent Behavior
|
|
858
|
+
|
|
859
|
+
Complex system behavior from simple agent rules:
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
# Temperature agent: "Keep temperature at 22°C"
|
|
863
|
+
# Energy agent: "Don't exceed 5000W"
|
|
864
|
+
# Arbitration agent: "Comfort during occupied hours"
|
|
865
|
+
# Result: System automatically balances comfort and efficiency
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## Multi-Agent Patterns
|
|
869
|
+
|
|
870
|
+
### Agent Roles
|
|
871
|
+
|
|
872
|
+
**Reactive Agents**: Respond to immediate stimuli
|
|
873
|
+
|
|
874
|
+
```ruby
|
|
875
|
+
class ReactiveAgent < Agent
|
|
876
|
+
def process
|
|
877
|
+
# React to current sensor readings
|
|
878
|
+
sensor_facts = @engine.facts.select { |f| f.type == :sensor_reading }
|
|
879
|
+
sensor_facts.each { |fact| react_to(fact) }
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**Proactive Agents**: Plan and execute goals
|
|
885
|
+
|
|
886
|
+
```ruby
|
|
887
|
+
class ProactiveAgent < Agent
|
|
888
|
+
def process
|
|
889
|
+
# Check goals
|
|
890
|
+
goals = @engine.facts.select { |f| f.type == :goal }
|
|
891
|
+
goals.each { |goal| plan_for(goal) }
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
**Social Agents**: Collaborate with other agents
|
|
897
|
+
|
|
898
|
+
```ruby
|
|
899
|
+
class SocialAgent < Agent
|
|
900
|
+
def process
|
|
901
|
+
# Coordinate with other agents
|
|
902
|
+
messages = receive_messages(:coordination)
|
|
903
|
+
messages.each { |msg| coordinate_with(msg) }
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Communication Protocols
|
|
909
|
+
|
|
910
|
+
**Request-Reply**:
|
|
911
|
+
|
|
912
|
+
```ruby
|
|
913
|
+
# Agent A sends request
|
|
914
|
+
send_message(:task_queue, {
|
|
915
|
+
type: "request",
|
|
916
|
+
request_id: SecureRandom.uuid,
|
|
917
|
+
action: "analyze_data"
|
|
918
|
+
}, priority: 50)
|
|
919
|
+
|
|
920
|
+
# Agent B processes and replies
|
|
921
|
+
request = @engine.pop_message(:task_queue)
|
|
922
|
+
result = process_request(request)
|
|
923
|
+
|
|
924
|
+
send_message(:responses, {
|
|
925
|
+
type: "reply",
|
|
926
|
+
request_id: request[:content][:request_id],
|
|
927
|
+
result: result
|
|
928
|
+
}, priority: 60)
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
**Broadcast**:
|
|
932
|
+
|
|
933
|
+
```ruby
|
|
934
|
+
# Agent sends to all
|
|
935
|
+
@engine.add_fact(:broadcast, {
|
|
936
|
+
from: @name,
|
|
937
|
+
message: "System shutdown in 5 minutes"
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
# All agents read
|
|
941
|
+
broadcasts = @engine.facts.select { |f|
|
|
942
|
+
f.type == :broadcast && f[:from] != @name
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
**Negotiation**:
|
|
947
|
+
|
|
948
|
+
```ruby
|
|
949
|
+
# Agent proposes
|
|
950
|
+
@engine.add_fact(:proposal, {
|
|
951
|
+
from: @name,
|
|
952
|
+
resource: "hvac_bedroom",
|
|
953
|
+
duration: 30,
|
|
954
|
+
priority: 5
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
# Other agents bid or decline
|
|
958
|
+
existing_proposals = @engine.facts.select { |f| f.type == :proposal }
|
|
959
|
+
if can_accept?(existing_proposals)
|
|
960
|
+
@engine.add_fact(:acceptance, { proposal_id: proposal.id })
|
|
961
|
+
else
|
|
962
|
+
@engine.add_fact(:counter_proposal, { ... })
|
|
963
|
+
end
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Coordination Strategies
|
|
967
|
+
|
|
968
|
+
**Centralized Coordination**:
|
|
969
|
+
|
|
970
|
+
```ruby
|
|
971
|
+
class CoordinatorAgent < Agent
|
|
972
|
+
def process
|
|
973
|
+
# Collect all agent requests
|
|
974
|
+
requests = []
|
|
975
|
+
@agents.each do |agent|
|
|
976
|
+
while (msg = @engine.pop_message(:"#{agent.name}_requests"))
|
|
977
|
+
requests << msg
|
|
978
|
+
end
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# Optimize schedule
|
|
982
|
+
schedule = optimize_schedule(requests)
|
|
983
|
+
|
|
984
|
+
# Dispatch commands
|
|
985
|
+
schedule.each do |task|
|
|
986
|
+
send_message(:"#{task[:agent]}_commands", task, priority: 70)
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
**Distributed Coordination**:
|
|
993
|
+
|
|
994
|
+
```ruby
|
|
995
|
+
class DistributedAgent < Agent
|
|
996
|
+
def process
|
|
997
|
+
# Each agent coordinates locally
|
|
998
|
+
neighbors = find_neighbors
|
|
999
|
+
neighbors.each do |neighbor|
|
|
1000
|
+
send_message(:"#{neighbor}_coordination", {
|
|
1001
|
+
from: @name,
|
|
1002
|
+
state: current_state
|
|
1003
|
+
}, priority: 50)
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Adjust based on neighbor states
|
|
1007
|
+
neighbor_states = receive_messages(:"#{@name}_coordination")
|
|
1008
|
+
adjust_behavior(neighbor_states)
|
|
1009
|
+
end
|
|
1010
|
+
end
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Market-Based Coordination**:
|
|
1014
|
+
|
|
1015
|
+
```ruby
|
|
1016
|
+
class MarketAgent < Agent
|
|
1017
|
+
def process
|
|
1018
|
+
# Bid on tasks based on cost
|
|
1019
|
+
tasks = @engine.facts.select { |f| f.type == :available_task }
|
|
1020
|
+
|
|
1021
|
+
tasks.each do |task|
|
|
1022
|
+
cost = calculate_cost(task)
|
|
1023
|
+
@engine.add_fact(:bid, {
|
|
1024
|
+
agent: @name,
|
|
1025
|
+
task_id: task.id,
|
|
1026
|
+
cost: cost
|
|
1027
|
+
})
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
# Winner takes task
|
|
1031
|
+
my_bids = @engine.facts.select { |f|
|
|
1032
|
+
f.type == :bid && f[:agent] == @name
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
my_bids.each do |bid|
|
|
1036
|
+
if winning_bid?(bid)
|
|
1037
|
+
execute_task(bid[:task_id])
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
end
|
|
1041
|
+
end
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
## Advanced Features
|
|
1045
|
+
|
|
1046
|
+
### Agent Discovery
|
|
1047
|
+
|
|
1048
|
+
```ruby
|
|
1049
|
+
def discover_agents
|
|
1050
|
+
agent_statuses = @engine.facts.select { |f| f.type == :agent_status }
|
|
1051
|
+
active_agents = agent_statuses.select { |a| a[:status] == "started" }
|
|
1052
|
+
|
|
1053
|
+
active_agents.map { |a| a[:agent] }
|
|
1054
|
+
end
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Dynamic Agent Creation
|
|
1058
|
+
|
|
1059
|
+
```ruby
|
|
1060
|
+
def spawn_agent(agent_class, *args)
|
|
1061
|
+
new_agent = agent_class.new(@engine, *args)
|
|
1062
|
+
@agents << new_agent
|
|
1063
|
+
new_agent.start
|
|
1064
|
+
new_agent
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# Example: Spawn specialist agent
|
|
1068
|
+
if complex_problem_detected?
|
|
1069
|
+
specialist = spawn_agent(SpecialistAgent, problem_type)
|
|
1070
|
+
end
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### Agent Lifecycle Management
|
|
1074
|
+
|
|
1075
|
+
```ruby
|
|
1076
|
+
class AgentManager
|
|
1077
|
+
def initialize(engine)
|
|
1078
|
+
@engine = engine
|
|
1079
|
+
@agents = {}
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def register(agent)
|
|
1083
|
+
@agents[agent.name] = agent
|
|
1084
|
+
|
|
1085
|
+
@engine.add_fact(:agent_registered, {
|
|
1086
|
+
name: agent.name,
|
|
1087
|
+
type: agent.class.name,
|
|
1088
|
+
timestamp: Time.now
|
|
1089
|
+
})
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def deregister(agent_name)
|
|
1093
|
+
agent = @agents.delete(agent_name)
|
|
1094
|
+
agent&.stop
|
|
1095
|
+
|
|
1096
|
+
@engine.add_fact(:agent_deregistered, {
|
|
1097
|
+
name: agent_name,
|
|
1098
|
+
timestamp: Time.now
|
|
1099
|
+
})
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def monitor_agents
|
|
1103
|
+
@agents.each do |name, agent|
|
|
1104
|
+
unless agent.running?
|
|
1105
|
+
# Restart failed agent
|
|
1106
|
+
agent.start
|
|
1107
|
+
@engine.add_fact(:agent_restarted, {
|
|
1108
|
+
name: name,
|
|
1109
|
+
timestamp: Time.now
|
|
1110
|
+
})
|
|
1111
|
+
end
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
end
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### Fault Tolerance
|
|
1118
|
+
|
|
1119
|
+
```ruby
|
|
1120
|
+
class FaultTolerantAgent < Agent
|
|
1121
|
+
def run_cycle
|
|
1122
|
+
return unless running?
|
|
1123
|
+
|
|
1124
|
+
begin
|
|
1125
|
+
process
|
|
1126
|
+
rescue => e
|
|
1127
|
+
handle_error(e)
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def handle_error(error)
|
|
1132
|
+
@engine.add_fact(:agent_error, {
|
|
1133
|
+
agent: @name,
|
|
1134
|
+
error: error.message,
|
|
1135
|
+
timestamp: Time.now
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
send_message(:system_alerts, {
|
|
1139
|
+
type: "agent_failure",
|
|
1140
|
+
agent: @name,
|
|
1141
|
+
error: error.message
|
|
1142
|
+
}, priority: 100)
|
|
1143
|
+
|
|
1144
|
+
# Attempt recovery
|
|
1145
|
+
attempt_recovery
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
def attempt_recovery
|
|
1149
|
+
# Restart agent's subsystems
|
|
1150
|
+
# Or notify manager for replacement
|
|
1151
|
+
end
|
|
1152
|
+
end
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
## Testing
|
|
1156
|
+
|
|
1157
|
+
```ruby
|
|
1158
|
+
require 'minitest/autorun'
|
|
1159
|
+
|
|
1160
|
+
class TestMultiAgentSystem < Minitest::Test
|
|
1161
|
+
def setup
|
|
1162
|
+
@system = SmartHomeSystem.new(db_path: ':memory:')
|
|
1163
|
+
@system.start
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
def teardown
|
|
1167
|
+
@system.stop
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def test_temperature_agent_creates_alert
|
|
1171
|
+
@system.add_sensor_reading("temperature", {
|
|
1172
|
+
location: "bedroom",
|
|
1173
|
+
value: 26.0
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1176
|
+
@system.run_cycle
|
|
1177
|
+
|
|
1178
|
+
alerts = @system.engine.facts.select { |f| f.type == :temperature_alert }
|
|
1179
|
+
|
|
1180
|
+
assert_equal 1, alerts.size
|
|
1181
|
+
assert_equal "bedroom", alerts.first[:location]
|
|
1182
|
+
assert_equal "high", alerts.first[:severity]
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def test_security_agent_detects_intrusion
|
|
1186
|
+
@system.engine.add_fact(:occupancy, { status: "away" })
|
|
1187
|
+
|
|
1188
|
+
@system.add_sensor_reading("motion", {
|
|
1189
|
+
location: "living_room",
|
|
1190
|
+
detected: true
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
@system.run_cycle
|
|
1194
|
+
|
|
1195
|
+
alerts = @system.engine.facts.select { |f|
|
|
1196
|
+
f.type == :security_alert && f[:type] == "intrusion"
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
assert_equal 1, alerts.size
|
|
1200
|
+
assert_equal "critical", alerts.first[:severity]
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
def test_energy_agent_triggers_reduction
|
|
1204
|
+
@system.add_sensor_reading("power", { value: 6000 })
|
|
1205
|
+
|
|
1206
|
+
@system.run_cycle
|
|
1207
|
+
|
|
1208
|
+
energy_alerts = @system.engine.facts.select { |f|
|
|
1209
|
+
f.type == :energy_alert
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
assert_equal 1, energy_alerts.size
|
|
1213
|
+
|
|
1214
|
+
# Check for reduction message
|
|
1215
|
+
msg = @system.engine.pop_message(:device_control)
|
|
1216
|
+
assert msg
|
|
1217
|
+
assert_equal "reduce_consumption", msg[:content][:content][:action]
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def test_arbitration_security_over_energy
|
|
1221
|
+
# Create conflict: security alert + energy alert
|
|
1222
|
+
@system.engine.add_fact(:security_alert, {
|
|
1223
|
+
type: "intrusion",
|
|
1224
|
+
severity: "critical"
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
@system.add_sensor_reading("power", { value: 6000 })
|
|
1228
|
+
|
|
1229
|
+
@system.run_cycle
|
|
1230
|
+
|
|
1231
|
+
# Arbitration should send cancellation
|
|
1232
|
+
msg = @system.engine.pop_message(:device_control)
|
|
1233
|
+
while msg
|
|
1234
|
+
if msg[:content][:content][:action] == "cancel_energy_reduction"
|
|
1235
|
+
assert_equal "security_priority", msg[:content][:content][:reason]
|
|
1236
|
+
return
|
|
1237
|
+
end
|
|
1238
|
+
msg = @system.engine.pop_message(:device_control)
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
flunk "Expected cancellation message not found"
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
def test_schedule_agent_morning_routine
|
|
1245
|
+
@system.trigger_time_event("morning", { hour: 7 })
|
|
1246
|
+
|
|
1247
|
+
@system.run_cycle
|
|
1248
|
+
|
|
1249
|
+
# Check occupancy updated
|
|
1250
|
+
occupancy = @system.engine.facts.find { |f| f.type == :occupancy }
|
|
1251
|
+
assert_equal "home", occupancy[:status]
|
|
1252
|
+
|
|
1253
|
+
# Check routine executed
|
|
1254
|
+
routine = @system.engine.facts.find { |f|
|
|
1255
|
+
f.type == :routine_executed && f[:type] == "morning"
|
|
1256
|
+
}
|
|
1257
|
+
assert routine
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
def test_agent_coordination_via_messages
|
|
1261
|
+
temp_agent = @system.agents.find { |a| a.is_a?(TemperatureAgent) }
|
|
1262
|
+
|
|
1263
|
+
@system.add_sensor_reading("temperature", {
|
|
1264
|
+
location: "bedroom",
|
|
1265
|
+
value: 26.0
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
@system.run_cycle
|
|
1269
|
+
|
|
1270
|
+
# Temperature agent should send HVAC message
|
|
1271
|
+
msg = @system.engine.pop_message(:hvac_control)
|
|
1272
|
+
|
|
1273
|
+
assert msg
|
|
1274
|
+
assert_equal "TemperatureAgent", msg[:content][:from]
|
|
1275
|
+
assert_equal "cool", msg[:content][:content][:action]
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
def test_system_status
|
|
1279
|
+
status = @system.status
|
|
1280
|
+
|
|
1281
|
+
assert status[:running]
|
|
1282
|
+
assert_equal 5, status[:agents].size
|
|
1283
|
+
assert status[:agents].all? { |a| a[:running] }
|
|
1284
|
+
end
|
|
1285
|
+
end
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
## Performance Considerations
|
|
1289
|
+
|
|
1290
|
+
### Use Redis for High-Throughput
|
|
1291
|
+
|
|
1292
|
+
```ruby
|
|
1293
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
1294
|
+
|
|
1295
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
1296
|
+
url: 'redis://localhost:6379/0'
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
system = SmartHomeSystem.new(store: store)
|
|
1300
|
+
# 100x faster message passing
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
### Agent Thread Pools
|
|
1304
|
+
|
|
1305
|
+
```ruby
|
|
1306
|
+
require 'concurrent'
|
|
1307
|
+
|
|
1308
|
+
class ThreadedSmartHomeSystem < SmartHomeSystem
|
|
1309
|
+
def run_continuous(interval: 1)
|
|
1310
|
+
pool = Concurrent::FixedThreadPool.new(5)
|
|
1311
|
+
|
|
1312
|
+
while @running
|
|
1313
|
+
@agents.each do |agent|
|
|
1314
|
+
pool.post { agent.run_cycle }
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
sleep interval
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
pool.shutdown
|
|
1321
|
+
pool.wait_for_termination
|
|
1322
|
+
end
|
|
1323
|
+
end
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
## Next Steps
|
|
1327
|
+
|
|
1328
|
+
- **[Blackboard Memory](../guides/blackboard-memory.md)** - Shared workspace details
|
|
1329
|
+
- **[Performance Guide](../advanced/performance.md)** - Optimize multi-agent systems
|
|
1330
|
+
- **[Testing Guide](../advanced/testing.md)** - Test agent interactions
|
|
1331
|
+
- **[API Reference](../api/blackboard.md)** - Blackboard API
|
|
1332
|
+
|
|
1333
|
+
---
|
|
1334
|
+
|
|
1335
|
+
*Multi-agent systems enable emergent intelligence through collaboration. Each agent contributes specialized expertise, and the system as a whole solves problems no single agent could solve alone.*
|