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
data/docs/guides/dsl.md
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
# DSL Reference Guide
|
|
2
|
+
|
|
3
|
+
Complete reference for the KBS Domain-Specific Language for defining knowledge bases and rules.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Start](#quick-start)
|
|
8
|
+
- [Knowledge Base](#knowledge-base)
|
|
9
|
+
- [Rule Definition](#rule-definition)
|
|
10
|
+
- [Condition Syntax](#condition-syntax)
|
|
11
|
+
- [Pattern Helpers](#pattern-helpers)
|
|
12
|
+
- [Variable Binding](#variable-binding)
|
|
13
|
+
- [Negation](#negation)
|
|
14
|
+
- [Actions](#actions)
|
|
15
|
+
- [Working with Facts](#working-with-facts)
|
|
16
|
+
- [Introspection](#introspection)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
The KBS DSL provides a natural, English-like syntax for defining knowledge-based systems:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'kbs'
|
|
26
|
+
|
|
27
|
+
kb = KBS.knowledge_base do
|
|
28
|
+
# Define a rule
|
|
29
|
+
rule "high_temperature_alert" do
|
|
30
|
+
desc "Alert when temperature exceeds threshold"
|
|
31
|
+
priority 10
|
|
32
|
+
|
|
33
|
+
on :temperature, value: greater_than(80), location: :loc?
|
|
34
|
+
|
|
35
|
+
perform do |bindings|
|
|
36
|
+
puts "High temperature at #{bindings[:loc?]}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Add facts
|
|
41
|
+
fact :temperature, value: 85, location: "server_room"
|
|
42
|
+
|
|
43
|
+
# Execute rules
|
|
44
|
+
run
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Knowledge Base
|
|
51
|
+
|
|
52
|
+
### Creating a Knowledge Base
|
|
53
|
+
|
|
54
|
+
#### `KBS.knowledge_base(&block)`
|
|
55
|
+
|
|
56
|
+
Creates a new knowledge base and evaluates the block in its context.
|
|
57
|
+
|
|
58
|
+
**Returns**: `KBS::DSL::KnowledgeBase` instance
|
|
59
|
+
|
|
60
|
+
**Example**:
|
|
61
|
+
```ruby
|
|
62
|
+
kb = KBS.knowledge_base do
|
|
63
|
+
# Define rules and add facts here
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Access the underlying engine
|
|
67
|
+
kb.engine # => KBS::Engine
|
|
68
|
+
|
|
69
|
+
# Access defined rules
|
|
70
|
+
kb.rules # => Hash of rule_name => KBS::Rule
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Knowledge Base Methods
|
|
76
|
+
|
|
77
|
+
#### `rule(name, &block)` / `defrule(name, &block)`
|
|
78
|
+
|
|
79
|
+
Defines a new rule.
|
|
80
|
+
|
|
81
|
+
**Parameters**:
|
|
82
|
+
- `name` (String or Symbol) - Rule name
|
|
83
|
+
- `&block` - Block containing rule definition
|
|
84
|
+
|
|
85
|
+
**Returns**: `KBS::DSL::RuleBuilder`
|
|
86
|
+
|
|
87
|
+
**Example**:
|
|
88
|
+
```ruby
|
|
89
|
+
kb = KBS.knowledge_base do
|
|
90
|
+
rule "example_rule" do
|
|
91
|
+
# Rule definition here
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Alias
|
|
95
|
+
defrule "another_rule" do
|
|
96
|
+
# Rule definition here
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
#### `fact(type, attributes = {})` / `assert(type, attributes = {})`
|
|
104
|
+
|
|
105
|
+
Adds a fact to working memory.
|
|
106
|
+
|
|
107
|
+
**Parameters**:
|
|
108
|
+
- `type` (Symbol) - Fact type
|
|
109
|
+
- `attributes` (Hash) - Fact attributes
|
|
110
|
+
|
|
111
|
+
**Returns**: `KBS::Fact`
|
|
112
|
+
|
|
113
|
+
**Example**:
|
|
114
|
+
```ruby
|
|
115
|
+
kb = KBS.knowledge_base do
|
|
116
|
+
fact :temperature, value: 85, location: "server_room"
|
|
117
|
+
|
|
118
|
+
# Alias
|
|
119
|
+
assert :sensor, id: 1, status: "active"
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
#### `retract(fact)`
|
|
126
|
+
|
|
127
|
+
Removes a fact from working memory.
|
|
128
|
+
|
|
129
|
+
**Parameters**:
|
|
130
|
+
- `fact` (KBS::Fact) - Fact to remove
|
|
131
|
+
|
|
132
|
+
**Returns**: `nil`
|
|
133
|
+
|
|
134
|
+
**Example**:
|
|
135
|
+
```ruby
|
|
136
|
+
kb = KBS.knowledge_base do
|
|
137
|
+
temp_fact = fact :temperature, value: 85
|
|
138
|
+
|
|
139
|
+
# Later...
|
|
140
|
+
retract temp_fact
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
#### `run()`
|
|
147
|
+
|
|
148
|
+
Executes all activated rules.
|
|
149
|
+
|
|
150
|
+
**Returns**: `nil`
|
|
151
|
+
|
|
152
|
+
**Example**:
|
|
153
|
+
```ruby
|
|
154
|
+
kb = KBS.knowledge_base do
|
|
155
|
+
rule "my_rule" do
|
|
156
|
+
on :temperature, value: greater_than(80)
|
|
157
|
+
perform { puts "High temperature!" }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
fact :temperature, value: 85
|
|
161
|
+
|
|
162
|
+
run # Fires "my_rule"
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
#### `reset()`
|
|
169
|
+
|
|
170
|
+
Clears all facts from working memory.
|
|
171
|
+
|
|
172
|
+
**Returns**: `nil`
|
|
173
|
+
|
|
174
|
+
**Example**:
|
|
175
|
+
```ruby
|
|
176
|
+
kb = KBS.knowledge_base do
|
|
177
|
+
fact :temperature, value: 85
|
|
178
|
+
fact :humidity, value: 60
|
|
179
|
+
|
|
180
|
+
reset # All facts removed
|
|
181
|
+
|
|
182
|
+
puts facts.size # => 0
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
#### `facts()`
|
|
189
|
+
|
|
190
|
+
Returns all facts in working memory.
|
|
191
|
+
|
|
192
|
+
**Returns**: `Array<KBS::Fact>`
|
|
193
|
+
|
|
194
|
+
**Example**:
|
|
195
|
+
```ruby
|
|
196
|
+
kb = KBS.knowledge_base do
|
|
197
|
+
fact :temperature, value: 85
|
|
198
|
+
fact :humidity, value: 60
|
|
199
|
+
|
|
200
|
+
puts facts.size # => 2
|
|
201
|
+
|
|
202
|
+
facts.each do |f|
|
|
203
|
+
puts "#{f.type}: #{f.attributes}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
#### `query(type, pattern = {})`
|
|
211
|
+
|
|
212
|
+
Queries facts by type and attributes.
|
|
213
|
+
|
|
214
|
+
**Parameters**:
|
|
215
|
+
- `type` (Symbol) - Fact type to match
|
|
216
|
+
- `pattern` (Hash) - Attribute key-value pairs to match
|
|
217
|
+
|
|
218
|
+
**Returns**: `Array<KBS::Fact>`
|
|
219
|
+
|
|
220
|
+
**Example**:
|
|
221
|
+
```ruby
|
|
222
|
+
kb = KBS.knowledge_base do
|
|
223
|
+
fact :temperature, value: 85, location: "server_room"
|
|
224
|
+
fact :temperature, value: 75, location: "lobby"
|
|
225
|
+
fact :humidity, value: 60, location: "server_room"
|
|
226
|
+
|
|
227
|
+
# Find all temperature facts
|
|
228
|
+
temps = query(:temperature)
|
|
229
|
+
puts temps.size # => 2
|
|
230
|
+
|
|
231
|
+
# Find temperature facts in server_room
|
|
232
|
+
server_temps = query(:temperature, location: "server_room")
|
|
233
|
+
puts server_temps.size # => 1
|
|
234
|
+
puts server_temps.first[:value] # => 85
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
#### `print_facts()`
|
|
241
|
+
|
|
242
|
+
Displays all facts in working memory.
|
|
243
|
+
|
|
244
|
+
**Returns**: `nil`
|
|
245
|
+
|
|
246
|
+
**Example**:
|
|
247
|
+
```ruby
|
|
248
|
+
kb = KBS.knowledge_base do
|
|
249
|
+
fact :temperature, value: 85
|
|
250
|
+
fact :humidity, value: 60
|
|
251
|
+
|
|
252
|
+
print_facts
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Output:
|
|
256
|
+
# Working Memory Contents:
|
|
257
|
+
# ----------------------------------------
|
|
258
|
+
# 1. temperature(value: 85)
|
|
259
|
+
# 2. humidity(value: 60)
|
|
260
|
+
# ----------------------------------------
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
#### `print_rules()`
|
|
266
|
+
|
|
267
|
+
Displays all defined rules with their conditions.
|
|
268
|
+
|
|
269
|
+
**Returns**: `nil`
|
|
270
|
+
|
|
271
|
+
**Example**:
|
|
272
|
+
```ruby
|
|
273
|
+
kb = KBS.knowledge_base do
|
|
274
|
+
rule "high_temp" do
|
|
275
|
+
desc "Alert on high temperature"
|
|
276
|
+
priority 10
|
|
277
|
+
on :temperature, value: greater_than(80)
|
|
278
|
+
perform { puts "High temp!" }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
print_rules
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Output:
|
|
285
|
+
# Knowledge Base Rules:
|
|
286
|
+
# ----------------------------------------
|
|
287
|
+
# Rule: high_temp
|
|
288
|
+
# Description: Alert on high temperature
|
|
289
|
+
# Priority: 10
|
|
290
|
+
# Conditions: 1
|
|
291
|
+
# 1. temperature({:value=>#<Proc:...>})
|
|
292
|
+
# ----------------------------------------
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Rule Definition
|
|
298
|
+
|
|
299
|
+
Rules are defined using the `rule` method with a block containing:
|
|
300
|
+
|
|
301
|
+
1. **Metadata**: Description and priority
|
|
302
|
+
2. **Conditions**: Patterns to match facts
|
|
303
|
+
3. **Action**: Code to execute when all conditions match
|
|
304
|
+
|
|
305
|
+
### Rule Structure
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
rule "rule_name" do
|
|
309
|
+
desc "Optional description"
|
|
310
|
+
priority 10 # Optional, default: 0
|
|
311
|
+
|
|
312
|
+
# Conditions (one or more)
|
|
313
|
+
on :fact_type, attribute: value, other: :variable?
|
|
314
|
+
on :another_type, field: predicate
|
|
315
|
+
|
|
316
|
+
# Action
|
|
317
|
+
perform do |bindings|
|
|
318
|
+
# Code to execute
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### Rule Metadata
|
|
326
|
+
|
|
327
|
+
#### `desc(description)`
|
|
328
|
+
|
|
329
|
+
Sets the rule description (for documentation and debugging).
|
|
330
|
+
|
|
331
|
+
**Parameters**:
|
|
332
|
+
- `description` (String) - Human-readable description
|
|
333
|
+
|
|
334
|
+
**Returns**: `self` (chainable)
|
|
335
|
+
|
|
336
|
+
**Example**:
|
|
337
|
+
```ruby
|
|
338
|
+
rule "temperature_alert" do
|
|
339
|
+
desc "Alerts when server room temperature exceeds safe threshold"
|
|
340
|
+
|
|
341
|
+
on :temperature, location: "server_room", value: greater_than(80)
|
|
342
|
+
perform { puts "High temperature alert!" }
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
#### `priority(level)`
|
|
349
|
+
|
|
350
|
+
Sets the rule priority (higher priority rules fire first).
|
|
351
|
+
|
|
352
|
+
**Parameters**:
|
|
353
|
+
- `level` (Integer) - Priority level (default: 0)
|
|
354
|
+
|
|
355
|
+
**Returns**: `self` (chainable)
|
|
356
|
+
|
|
357
|
+
**Note**: Priority only affects execution order in `KBS::Blackboard::Engine`, not `KBS::Engine`.
|
|
358
|
+
|
|
359
|
+
**Example**:
|
|
360
|
+
```ruby
|
|
361
|
+
rule "critical_shutdown" do
|
|
362
|
+
priority 1000 # Highest priority
|
|
363
|
+
on :temperature, value: greater_than(120)
|
|
364
|
+
perform { shutdown_system! }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
rule "log_reading" do
|
|
368
|
+
priority 1 # Low priority
|
|
369
|
+
on :temperature, value: :temp?
|
|
370
|
+
perform { |b| log(b[:temp?]) }
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Condition Syntax
|
|
377
|
+
|
|
378
|
+
Conditions specify patterns that must match facts in working memory.
|
|
379
|
+
|
|
380
|
+
### Condition Keywords
|
|
381
|
+
|
|
382
|
+
All of these are **aliases** - use whichever reads best for your domain:
|
|
383
|
+
|
|
384
|
+
- **`on(type, pattern = {}, &block)`** - Primary keyword
|
|
385
|
+
- **`given(type, pattern = {})`** - Alias for `on`
|
|
386
|
+
- **`matches(type, pattern = {})`** - Alias for `on`
|
|
387
|
+
- **`fact(type, pattern = {})`** - Alias for `on`
|
|
388
|
+
- **`exists(type, pattern = {})`** - Alias for `on`
|
|
389
|
+
|
|
390
|
+
**Parameters**:
|
|
391
|
+
- `type` (Symbol) - Fact type to match
|
|
392
|
+
- `pattern` (Hash) - Attribute constraints (key-value pairs)
|
|
393
|
+
- `&block` (optional) - Block-style pattern definition
|
|
394
|
+
|
|
395
|
+
**Returns**: `self` (chainable)
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### Basic Condition Examples
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Match any temperature fact
|
|
403
|
+
on :temperature
|
|
404
|
+
|
|
405
|
+
# Match temperature with specific value
|
|
406
|
+
on :temperature, value: 85
|
|
407
|
+
|
|
408
|
+
# Match temperature with multiple attributes
|
|
409
|
+
on :temperature, value: 85, location: "server_room"
|
|
410
|
+
|
|
411
|
+
# Using aliases
|
|
412
|
+
given :sensor, status: "active"
|
|
413
|
+
matches :order, status: "pending"
|
|
414
|
+
fact :inventory, quantity: 0
|
|
415
|
+
exists :alert, level: "critical"
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### Literal Matching
|
|
421
|
+
|
|
422
|
+
Match exact attribute values:
|
|
423
|
+
|
|
424
|
+
```ruby
|
|
425
|
+
on :temperature, location: "server_room" # location must equal "server_room"
|
|
426
|
+
on :order, status: "pending", total: 100 # Both must match exactly
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### Variable Binding
|
|
432
|
+
|
|
433
|
+
Capture attribute values in variables (symbols starting with `?`):
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
on :temperature, value: :temp?, location: :loc?
|
|
437
|
+
|
|
438
|
+
# In action:
|
|
439
|
+
perform do |bindings|
|
|
440
|
+
puts "Temperature: #{bindings[:temp?]}"
|
|
441
|
+
puts "Location: #{bindings[:loc?]}"
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Join Test**: Same variable in multiple conditions creates a join:
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
on :order, product_id: :pid?, quantity: :qty?
|
|
449
|
+
on :inventory, product_id: :pid?, available: :avail?
|
|
450
|
+
|
|
451
|
+
# These conditions only match when product_id is the same in both facts
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
### Predicate Matching
|
|
457
|
+
|
|
458
|
+
Use lambdas or helper methods for complex conditions:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
# Lambda predicate
|
|
462
|
+
on :temperature, value: ->(v) { v > 80 && v < 100 }
|
|
463
|
+
|
|
464
|
+
# Helper method (see Pattern Helpers section)
|
|
465
|
+
on :temperature, value: greater_than(80)
|
|
466
|
+
on :order, total: between(100, 1000)
|
|
467
|
+
on :status, code: one_of("pending", "processing", "completed")
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### Block-Style Patterns
|
|
473
|
+
|
|
474
|
+
Define patterns using a block with method-missing magic:
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
on :temperature do
|
|
478
|
+
value > 80 # Creates lambda: ->(v) { v > 80 }
|
|
479
|
+
location :loc? # Binds variable
|
|
480
|
+
sensor_id 42 # Literal match
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Equivalent to:
|
|
484
|
+
on :temperature,
|
|
485
|
+
value: ->(v) { v > 80 },
|
|
486
|
+
location: :loc?,
|
|
487
|
+
sensor_id: 42
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Available operators in blocks**:
|
|
491
|
+
- `>`, `<`, `>=`, `<=` - Comparison operators
|
|
492
|
+
- `==` - Equality (same as literal value)
|
|
493
|
+
- `!=` - Inequality
|
|
494
|
+
- `between(min, max)` - Range check
|
|
495
|
+
- `in(collection)` - Membership check
|
|
496
|
+
- `matches(pattern)` - Regex match
|
|
497
|
+
- `any(*values)` - Match any of the values
|
|
498
|
+
- `all(*conditions)` - All conditions must be true
|
|
499
|
+
|
|
500
|
+
**Example**:
|
|
501
|
+
```ruby
|
|
502
|
+
on :order do
|
|
503
|
+
total > 1000
|
|
504
|
+
status in ["pending", "processing"]
|
|
505
|
+
customer_email matches(/@example\.com$/)
|
|
506
|
+
priority any(1, 2, 3)
|
|
507
|
+
end
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Pattern Helpers
|
|
513
|
+
|
|
514
|
+
Helper methods available in rule conditions (from `ConditionHelpers` module).
|
|
515
|
+
|
|
516
|
+
### Comparison Helpers
|
|
517
|
+
|
|
518
|
+
#### `greater_than(value)`
|
|
519
|
+
|
|
520
|
+
Matches values greater than the specified value.
|
|
521
|
+
|
|
522
|
+
**Example**:
|
|
523
|
+
```ruby
|
|
524
|
+
on :temperature, value: greater_than(80)
|
|
525
|
+
# Equivalent to: value: ->(v) { v > 80 }
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
#### `less_than(value)`
|
|
531
|
+
|
|
532
|
+
Matches values less than the specified value.
|
|
533
|
+
|
|
534
|
+
**Example**:
|
|
535
|
+
```ruby
|
|
536
|
+
on :inventory, quantity: less_than(10)
|
|
537
|
+
# Equivalent to: quantity: ->(q) { q < 10 }
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
#### `equals(value)`
|
|
543
|
+
|
|
544
|
+
Explicitly matches an exact value (same as literal).
|
|
545
|
+
|
|
546
|
+
**Example**:
|
|
547
|
+
```ruby
|
|
548
|
+
on :sensor, status: equals("active")
|
|
549
|
+
# Equivalent to: status: "active"
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
#### `not_equal(value)`
|
|
555
|
+
|
|
556
|
+
Matches values not equal to the specified value.
|
|
557
|
+
|
|
558
|
+
**Example**:
|
|
559
|
+
```ruby
|
|
560
|
+
on :order, status: not_equal("cancelled")
|
|
561
|
+
# Equivalent to: status: ->(s) { s != "cancelled" }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
### Range Helpers
|
|
567
|
+
|
|
568
|
+
#### `between(min, max)` / `range(min, max)`
|
|
569
|
+
|
|
570
|
+
Matches values in an inclusive range.
|
|
571
|
+
|
|
572
|
+
**Example**:
|
|
573
|
+
```ruby
|
|
574
|
+
on :temperature, value: between(70, 90)
|
|
575
|
+
# Equivalent to: value: ->(v) { v >= 70 && v <= 90 }
|
|
576
|
+
|
|
577
|
+
# Also works with Range objects:
|
|
578
|
+
on :temperature, value: range(70..90)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
### Collection Helpers
|
|
584
|
+
|
|
585
|
+
#### `one_of(*values)`
|
|
586
|
+
|
|
587
|
+
Matches if value is one of the specified values.
|
|
588
|
+
|
|
589
|
+
**Example**:
|
|
590
|
+
```ruby
|
|
591
|
+
on :order, status: one_of("pending", "processing", "completed")
|
|
592
|
+
# Equivalent to: status: ->(s) { ["pending", "processing", "completed"].include?(s) }
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
#### `any(*values)`
|
|
598
|
+
|
|
599
|
+
- With arguments: Same as `one_of`
|
|
600
|
+
- Without arguments: Matches any value (always true)
|
|
601
|
+
|
|
602
|
+
**Example**:
|
|
603
|
+
```ruby
|
|
604
|
+
# Match one of several values
|
|
605
|
+
on :priority, level: any(1, 2, 3)
|
|
606
|
+
|
|
607
|
+
# Match any value (always true)
|
|
608
|
+
on :metadata, extra_data: any
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
### String Helpers
|
|
614
|
+
|
|
615
|
+
#### `matches(pattern)`
|
|
616
|
+
|
|
617
|
+
Matches strings against a regular expression.
|
|
618
|
+
|
|
619
|
+
**Example**:
|
|
620
|
+
```ruby
|
|
621
|
+
on :email, address: matches(/@example\.com$/)
|
|
622
|
+
# Equivalent to: address: ->(a) { a.match?(/@example\.com$/) }
|
|
623
|
+
|
|
624
|
+
on :sensor, name: matches(/^temp_\d+$/)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
### Custom Predicates
|
|
630
|
+
|
|
631
|
+
#### `satisfies(&block)`
|
|
632
|
+
|
|
633
|
+
Creates a custom predicate from a block.
|
|
634
|
+
|
|
635
|
+
**Example**:
|
|
636
|
+
```ruby
|
|
637
|
+
on :order, total: satisfies { |t| t > 100 && t % 10 == 0 }
|
|
638
|
+
# Equivalent to: total: ->(t) { t > 100 && t % 10 == 0 }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Variable Binding
|
|
644
|
+
|
|
645
|
+
Variables allow you to:
|
|
646
|
+
1. Capture attribute values for use in actions
|
|
647
|
+
2. Create join tests between conditions
|
|
648
|
+
|
|
649
|
+
### Variable Syntax
|
|
650
|
+
|
|
651
|
+
Variables are symbols starting with `?`:
|
|
652
|
+
|
|
653
|
+
```ruby
|
|
654
|
+
:temp? # Variable named "temp"
|
|
655
|
+
:location? # Variable named "location"
|
|
656
|
+
:pid? # Variable named "pid"
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### Capturing Values
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
rule "temperature_report" do
|
|
665
|
+
on :temperature, value: :temp?, location: :loc?, timestamp: :time?
|
|
666
|
+
|
|
667
|
+
perform do |bindings|
|
|
668
|
+
puts "Temperature at #{bindings[:loc?]}: #{bindings[:temp?]}°F"
|
|
669
|
+
puts "Recorded: #{bindings[:time?]}"
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
### Join Tests
|
|
677
|
+
|
|
678
|
+
Variables with the same name in different conditions create a join test:
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
rule "check_inventory" do
|
|
682
|
+
on :order, product_id: :pid?, quantity: :qty?
|
|
683
|
+
on :inventory, product_id: :pid?, available: :avail?
|
|
684
|
+
|
|
685
|
+
perform do |bindings|
|
|
686
|
+
if bindings[:avail?] < bindings[:qty?]
|
|
687
|
+
puts "Insufficient inventory for product #{bindings[:pid?]}"
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# This rule only fires when:
|
|
693
|
+
# 1. An order fact exists
|
|
694
|
+
# 2. An inventory fact exists
|
|
695
|
+
# 3. Both facts have the SAME product_id
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
### Multiple Bindings
|
|
701
|
+
|
|
702
|
+
```ruby
|
|
703
|
+
rule "sensor_temperature_correlation" do
|
|
704
|
+
on :sensor, id: :sensor_id?, location: :loc?, status: "active"
|
|
705
|
+
on :temperature, sensor_id: :sensor_id?, value: :temp?
|
|
706
|
+
on :reading, sensor_id: :sensor_id?, timestamp: :time?
|
|
707
|
+
|
|
708
|
+
perform do |bindings|
|
|
709
|
+
# All three facts share the same sensor_id
|
|
710
|
+
puts "Sensor #{bindings[:sensor_id?]} at #{bindings[:loc?]}"
|
|
711
|
+
puts "Reading: #{bindings[:temp?]}°F at #{bindings[:time?]}"
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## Negation
|
|
719
|
+
|
|
720
|
+
Negation matches when a pattern is **absent** from working memory.
|
|
721
|
+
|
|
722
|
+
### Negation Keywords
|
|
723
|
+
|
|
724
|
+
All of these are **aliases**:
|
|
725
|
+
|
|
726
|
+
- **`without(type, pattern = {})`** - Primary negation keyword
|
|
727
|
+
- **`absent(type, pattern = {})`** - Alias
|
|
728
|
+
- **`missing(type, pattern = {})`** - Alias
|
|
729
|
+
- **`lacks(type, pattern = {})`** - Alias
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
### Direct Negation
|
|
734
|
+
|
|
735
|
+
```ruby
|
|
736
|
+
# Fire when there is NO alert fact
|
|
737
|
+
rule "all_clear" do
|
|
738
|
+
on :system, status: "running"
|
|
739
|
+
without :alert
|
|
740
|
+
perform { puts "All systems normal" }
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# Fire when there is NO critical alert
|
|
744
|
+
rule "no_critical_alerts" do
|
|
745
|
+
without :alert, level: "critical"
|
|
746
|
+
perform { puts "No critical alerts" }
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Using aliases
|
|
750
|
+
absent :error
|
|
751
|
+
missing :problem, severity: "high"
|
|
752
|
+
lacks :maintenance_flag
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
### Chained Negation
|
|
758
|
+
|
|
759
|
+
Use `without` (without arguments) followed by `on`:
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
rule "example" do
|
|
763
|
+
on :order, status: "pending"
|
|
764
|
+
without.on :inventory, quantity: 0
|
|
765
|
+
perform { puts "Order can be fulfilled" }
|
|
766
|
+
end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
### Negation with Variables
|
|
772
|
+
|
|
773
|
+
Variables in negated conditions create "there is no fact with this value" tests:
|
|
774
|
+
|
|
775
|
+
```ruby
|
|
776
|
+
rule "no_matching_inventory" do
|
|
777
|
+
on :order, product_id: :pid?
|
|
778
|
+
without :inventory, product_id: :pid?
|
|
779
|
+
|
|
780
|
+
perform do |bindings|
|
|
781
|
+
puts "No inventory for product #{bindings[:pid?]}"
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Fires when:
|
|
786
|
+
# 1. An order exists with product_id=X
|
|
787
|
+
# 2. NO inventory fact exists with product_id=X
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
### Negation Examples
|
|
793
|
+
|
|
794
|
+
```ruby
|
|
795
|
+
# Guard condition - only process if no errors
|
|
796
|
+
rule "process_order" do
|
|
797
|
+
on :order, status: "pending"
|
|
798
|
+
without :error
|
|
799
|
+
perform { process_order }
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Detect missing required fact
|
|
803
|
+
rule "missing_configuration" do
|
|
804
|
+
on :system, initialized: true
|
|
805
|
+
without :config, loaded: true
|
|
806
|
+
perform { puts "WARNING: Configuration not loaded" }
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Timeout detection
|
|
810
|
+
rule "sensor_timeout" do
|
|
811
|
+
on :sensor, id: :sensor_id?, expected: true
|
|
812
|
+
without :reading, sensor_id: :sensor_id?
|
|
813
|
+
perform { |b| puts "Sensor #{b[:sensor_id?]} timeout" }
|
|
814
|
+
end
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Actions
|
|
820
|
+
|
|
821
|
+
Actions define what happens when all conditions match.
|
|
822
|
+
|
|
823
|
+
### Action Keywords
|
|
824
|
+
|
|
825
|
+
All of these are **aliases**:
|
|
826
|
+
|
|
827
|
+
- **`perform(&block)`** - Primary action keyword
|
|
828
|
+
- **`action(&block)`** - Alias
|
|
829
|
+
- **`execute(&block)`** - Alias
|
|
830
|
+
- **`then(&block)`** - Alias
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
### Action Block
|
|
835
|
+
|
|
836
|
+
Actions receive a `bindings` hash containing all variable bindings:
|
|
837
|
+
|
|
838
|
+
```ruby
|
|
839
|
+
rule "example" do
|
|
840
|
+
on :temperature, value: :temp?, location: :loc?
|
|
841
|
+
|
|
842
|
+
perform do |bindings|
|
|
843
|
+
temp = bindings[:temp?]
|
|
844
|
+
location = bindings[:loc?]
|
|
845
|
+
puts "Temperature at #{location}: #{temp}°F"
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
### Action Capabilities
|
|
853
|
+
|
|
854
|
+
Actions can:
|
|
855
|
+
|
|
856
|
+
1. **Read bindings**:
|
|
857
|
+
```ruby
|
|
858
|
+
perform do |bindings|
|
|
859
|
+
value = bindings[:temp?]
|
|
860
|
+
end
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
2. **Access the knowledge base** (via closure):
|
|
864
|
+
```ruby
|
|
865
|
+
kb = KBS.knowledge_base do
|
|
866
|
+
rule "add_fact_from_action" do
|
|
867
|
+
on :trigger, event: "start"
|
|
868
|
+
perform do
|
|
869
|
+
fact :process, status: "running" # Add new fact
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
3. **Call external methods**:
|
|
876
|
+
```ruby
|
|
877
|
+
perform do |bindings|
|
|
878
|
+
send_email_alert(bindings[:temp?])
|
|
879
|
+
log_to_database(bindings)
|
|
880
|
+
trigger_alarm if bindings[:level?] == "critical"
|
|
881
|
+
end
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
4. **Add/remove facts**:
|
|
885
|
+
```ruby
|
|
886
|
+
perform do |bindings|
|
|
887
|
+
# Add derived fact
|
|
888
|
+
fact :alert, level: "high", source: bindings[:sensor_id?]
|
|
889
|
+
|
|
890
|
+
# Remove triggering fact
|
|
891
|
+
old_fact = query(:trigger, event: "start").first
|
|
892
|
+
retract old_fact if old_fact
|
|
893
|
+
end
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
### Action Examples
|
|
899
|
+
|
|
900
|
+
```ruby
|
|
901
|
+
# Simple logging
|
|
902
|
+
rule "log_temperature" do
|
|
903
|
+
on :temperature, value: :temp?
|
|
904
|
+
perform { |b| puts "Temperature: #{b[:temp?]}" }
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# State machine transition
|
|
908
|
+
rule "pending_to_processing" do
|
|
909
|
+
on :order, id: :order_id?, status: "pending"
|
|
910
|
+
on :worker, status: "available", id: :worker_id?
|
|
911
|
+
|
|
912
|
+
perform do |bindings|
|
|
913
|
+
# Update order status
|
|
914
|
+
order = query(:order, id: bindings[:order_id?]).first
|
|
915
|
+
retract order
|
|
916
|
+
fact :order, id: bindings[:order_id?],
|
|
917
|
+
status: "processing",
|
|
918
|
+
worker_id: bindings[:worker_id?]
|
|
919
|
+
|
|
920
|
+
# Update worker status
|
|
921
|
+
worker = query(:worker, id: bindings[:worker_id?]).first
|
|
922
|
+
retract worker
|
|
923
|
+
fact :worker, id: bindings[:worker_id?], status: "busy"
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Aggregation
|
|
928
|
+
rule "daily_summary" do
|
|
929
|
+
on :trigger, event: "end_of_day"
|
|
930
|
+
|
|
931
|
+
perform do
|
|
932
|
+
temps = query(:temperature).map { |f| f[:value] }
|
|
933
|
+
avg = temps.sum / temps.size.to_f
|
|
934
|
+
|
|
935
|
+
fact :daily_summary,
|
|
936
|
+
date: Date.today,
|
|
937
|
+
avg_temp: avg,
|
|
938
|
+
max_temp: temps.max,
|
|
939
|
+
min_temp: temps.min
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
## Working with Facts
|
|
947
|
+
|
|
948
|
+
### Adding Facts
|
|
949
|
+
|
|
950
|
+
```ruby
|
|
951
|
+
kb = KBS.knowledge_base do
|
|
952
|
+
# During initialization
|
|
953
|
+
fact :temperature, value: 85, location: "server_room"
|
|
954
|
+
fact :sensor, id: 1, status: "active"
|
|
955
|
+
|
|
956
|
+
# Or from action blocks
|
|
957
|
+
rule "add_derived_fact" do
|
|
958
|
+
on :temperature, value: greater_than(80)
|
|
959
|
+
perform do
|
|
960
|
+
fact :alert, level: "high", timestamp: Time.now
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# After creation
|
|
966
|
+
kb.fact :temperature, value: 90
|
|
967
|
+
kb.assert :humidity, value: 60 # Alias
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
### Removing Facts
|
|
973
|
+
|
|
974
|
+
```ruby
|
|
975
|
+
kb = KBS.knowledge_base do
|
|
976
|
+
temp = fact :temperature, value: 85
|
|
977
|
+
|
|
978
|
+
# Remove specific fact
|
|
979
|
+
retract temp
|
|
980
|
+
|
|
981
|
+
# Remove from action
|
|
982
|
+
rule "cleanup" do
|
|
983
|
+
on :temperature, timestamp: less_than(Time.now - 3600)
|
|
984
|
+
perform do
|
|
985
|
+
old_facts = query(:temperature)
|
|
986
|
+
.select { |f| f[:timestamp] < Time.now - 3600 }
|
|
987
|
+
old_facts.each { |f| retract f }
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
### Querying Facts
|
|
996
|
+
|
|
997
|
+
```ruby
|
|
998
|
+
kb = KBS.knowledge_base do
|
|
999
|
+
fact :temperature, value: 85, location: "server_room"
|
|
1000
|
+
fact :temperature, value: 75, location: "lobby"
|
|
1001
|
+
fact :humidity, value: 60, location: "server_room"
|
|
1002
|
+
|
|
1003
|
+
# Get all facts
|
|
1004
|
+
all = facts
|
|
1005
|
+
|
|
1006
|
+
# Query by type
|
|
1007
|
+
temps = query(:temperature)
|
|
1008
|
+
|
|
1009
|
+
# Query by type and attributes
|
|
1010
|
+
server_room_temps = query(:temperature, location: "server_room")
|
|
1011
|
+
|
|
1012
|
+
# Use query results in actions
|
|
1013
|
+
rule "check_average" do
|
|
1014
|
+
on :trigger, event: "calculate_average"
|
|
1015
|
+
|
|
1016
|
+
perform do
|
|
1017
|
+
temps = query(:temperature).map { |f| f[:value] }
|
|
1018
|
+
avg = temps.sum / temps.size.to_f
|
|
1019
|
+
puts "Average temperature: #{avg.round(1)}°F"
|
|
1020
|
+
end
|
|
1021
|
+
end
|
|
1022
|
+
end
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
## Introspection
|
|
1028
|
+
|
|
1029
|
+
### Inspecting Facts
|
|
1030
|
+
|
|
1031
|
+
```ruby
|
|
1032
|
+
kb = KBS.knowledge_base do
|
|
1033
|
+
fact :temperature, value: 85
|
|
1034
|
+
fact :humidity, value: 60
|
|
1035
|
+
|
|
1036
|
+
print_facts
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# Output:
|
|
1040
|
+
# Working Memory Contents:
|
|
1041
|
+
# ----------------------------------------
|
|
1042
|
+
# 1. temperature(value: 85)
|
|
1043
|
+
# 2. humidity(value: 60)
|
|
1044
|
+
# ----------------------------------------
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
---
|
|
1048
|
+
|
|
1049
|
+
### Inspecting Rules
|
|
1050
|
+
|
|
1051
|
+
```ruby
|
|
1052
|
+
kb = KBS.knowledge_base do
|
|
1053
|
+
rule "high_temp" do
|
|
1054
|
+
desc "Alert on high temperature"
|
|
1055
|
+
priority 10
|
|
1056
|
+
on :temperature, value: greater_than(80)
|
|
1057
|
+
perform { puts "High!" }
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
print_rules
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Output:
|
|
1064
|
+
# Knowledge Base Rules:
|
|
1065
|
+
# ----------------------------------------
|
|
1066
|
+
# Rule: high_temp
|
|
1067
|
+
# Description: Alert on high temperature
|
|
1068
|
+
# Priority: 10
|
|
1069
|
+
# Conditions: 1
|
|
1070
|
+
# 1. temperature({:value=>#<Proc:...>})
|
|
1071
|
+
# ----------------------------------------
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
### Programmatic Access
|
|
1077
|
+
|
|
1078
|
+
```ruby
|
|
1079
|
+
kb = KBS.knowledge_base do
|
|
1080
|
+
rule "example" do
|
|
1081
|
+
on :temperature, value: :temp?
|
|
1082
|
+
perform { |b| puts b[:temp?] }
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
# Access rules
|
|
1087
|
+
kb.rules # => Hash { "example" => KBS::Rule }
|
|
1088
|
+
kb.rules["example"] # => KBS::Rule instance
|
|
1089
|
+
|
|
1090
|
+
# Access engine
|
|
1091
|
+
kb.engine # => KBS::Engine
|
|
1092
|
+
kb.engine.working_memory # => KBS::WorkingMemory
|
|
1093
|
+
kb.engine.rules # => Array<KBS::Rule>
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
## Complete Examples
|
|
1099
|
+
|
|
1100
|
+
### Temperature Monitoring
|
|
1101
|
+
|
|
1102
|
+
```ruby
|
|
1103
|
+
require 'kbs'
|
|
1104
|
+
|
|
1105
|
+
kb = KBS.knowledge_base do
|
|
1106
|
+
# Rules
|
|
1107
|
+
rule "high_temperature_alert" do
|
|
1108
|
+
desc "Alert when temperature exceeds safe threshold"
|
|
1109
|
+
priority 10
|
|
1110
|
+
|
|
1111
|
+
on :sensor, id: :sensor_id?, status: "active"
|
|
1112
|
+
on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
|
|
1113
|
+
without :alert, sensor_id: :sensor_id? # No existing alert
|
|
1114
|
+
|
|
1115
|
+
perform do |bindings|
|
|
1116
|
+
puts "⚠️ HIGH TEMPERATURE ALERT"
|
|
1117
|
+
puts "Sensor: #{bindings[:sensor_id?]}"
|
|
1118
|
+
puts "Temperature: #{bindings[:value?]}°F"
|
|
1119
|
+
|
|
1120
|
+
# Create alert fact
|
|
1121
|
+
fact :alert,
|
|
1122
|
+
sensor_id: bindings[:sensor_id?],
|
|
1123
|
+
level: "high",
|
|
1124
|
+
timestamp: Time.now
|
|
1125
|
+
end
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
rule "temperature_normal" do
|
|
1129
|
+
desc "Clear alert when temperature returns to normal"
|
|
1130
|
+
priority 5
|
|
1131
|
+
|
|
1132
|
+
on :temperature, sensor_id: :sensor_id?, value: less_than(75)
|
|
1133
|
+
on :alert, sensor_id: :sensor_id?
|
|
1134
|
+
|
|
1135
|
+
perform do |bindings|
|
|
1136
|
+
puts "✓ Temperature normal for sensor #{bindings[:sensor_id?]}"
|
|
1137
|
+
|
|
1138
|
+
# Remove alert
|
|
1139
|
+
alerts = query(:alert, sensor_id: bindings[:sensor_id?])
|
|
1140
|
+
alerts.each { |a| retract a }
|
|
1141
|
+
end
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
# Initial facts
|
|
1145
|
+
fact :sensor, id: 1, status: "active", location: "server_room"
|
|
1146
|
+
fact :sensor, id: 2, status: "active", location: "lobby"
|
|
1147
|
+
|
|
1148
|
+
# Simulate readings
|
|
1149
|
+
fact :temperature, sensor_id: 1, value: 85 # Will trigger alert
|
|
1150
|
+
fact :temperature, sensor_id: 2, value: 72 # Normal
|
|
1151
|
+
|
|
1152
|
+
run
|
|
1153
|
+
|
|
1154
|
+
print_facts
|
|
1155
|
+
end
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
|
|
1160
|
+
### Order Processing Workflow
|
|
1161
|
+
|
|
1162
|
+
```ruby
|
|
1163
|
+
kb = KBS.knowledge_base do
|
|
1164
|
+
rule "validate_order" do
|
|
1165
|
+
priority 100
|
|
1166
|
+
|
|
1167
|
+
on :order, id: :order_id?, status: "new", product_id: :pid?, quantity: :qty?
|
|
1168
|
+
on :inventory, product_id: :pid?, quantity: :available?
|
|
1169
|
+
|
|
1170
|
+
perform do |bindings|
|
|
1171
|
+
if bindings[:available?] >= bindings[:qty?]
|
|
1172
|
+
order = query(:order, id: bindings[:order_id?]).first
|
|
1173
|
+
retract order
|
|
1174
|
+
fact :order,
|
|
1175
|
+
id: bindings[:order_id?],
|
|
1176
|
+
status: "validated",
|
|
1177
|
+
product_id: bindings[:pid?],
|
|
1178
|
+
quantity: bindings[:qty?]
|
|
1179
|
+
else
|
|
1180
|
+
fact :alert,
|
|
1181
|
+
type: "insufficient_inventory",
|
|
1182
|
+
order_id: bindings[:order_id?]
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
rule "fulfill_order" do
|
|
1188
|
+
priority 50
|
|
1189
|
+
|
|
1190
|
+
on :order, id: :order_id?, status: "validated",
|
|
1191
|
+
product_id: :pid?, quantity: :qty?
|
|
1192
|
+
on :inventory, product_id: :pid?, quantity: :available?
|
|
1193
|
+
|
|
1194
|
+
perform do |bindings|
|
|
1195
|
+
# Deduct inventory
|
|
1196
|
+
inventory = query(:inventory, product_id: bindings[:pid?]).first
|
|
1197
|
+
retract inventory
|
|
1198
|
+
fact :inventory,
|
|
1199
|
+
product_id: bindings[:pid?],
|
|
1200
|
+
quantity: bindings[:available?] - bindings[:qty?]
|
|
1201
|
+
|
|
1202
|
+
# Update order status
|
|
1203
|
+
order = query(:order, id: bindings[:order_id?]).first
|
|
1204
|
+
retract order
|
|
1205
|
+
fact :order,
|
|
1206
|
+
id: bindings[:order_id?],
|
|
1207
|
+
status: "fulfilled",
|
|
1208
|
+
product_id: bindings[:pid?],
|
|
1209
|
+
quantity: bindings[:qty?]
|
|
1210
|
+
|
|
1211
|
+
puts "✓ Order #{bindings[:order_id?]} fulfilled"
|
|
1212
|
+
end
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
# Initial state
|
|
1216
|
+
fact :inventory, product_id: "ABC", quantity: 100
|
|
1217
|
+
fact :inventory, product_id: "XYZ", quantity: 50
|
|
1218
|
+
|
|
1219
|
+
fact :order, id: 1, status: "new", product_id: "ABC", quantity: 10
|
|
1220
|
+
fact :order, id: 2, status: "new", product_id: "XYZ", quantity: 60 # Insufficient!
|
|
1221
|
+
|
|
1222
|
+
run
|
|
1223
|
+
print_facts
|
|
1224
|
+
end
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1229
|
+
## Best Practices
|
|
1230
|
+
|
|
1231
|
+
### 1. Use Descriptive Names
|
|
1232
|
+
|
|
1233
|
+
```ruby
|
|
1234
|
+
# Good
|
|
1235
|
+
rule "high_temperature_alert" do
|
|
1236
|
+
desc "Alert when server room temperature exceeds 80°F"
|
|
1237
|
+
# ...
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
# Bad
|
|
1241
|
+
rule "rule1" do
|
|
1242
|
+
# ...
|
|
1243
|
+
end
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
---
|
|
1247
|
+
|
|
1248
|
+
### 2. Add Descriptions
|
|
1249
|
+
|
|
1250
|
+
```ruby
|
|
1251
|
+
rule "complex_calculation" do
|
|
1252
|
+
desc "Calculates portfolio value using current market prices and holdings"
|
|
1253
|
+
# ... complex logic ...
|
|
1254
|
+
end
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
---
|
|
1258
|
+
|
|
1259
|
+
### 3. Order Conditions by Selectivity
|
|
1260
|
+
|
|
1261
|
+
```ruby
|
|
1262
|
+
# Good - Most selective first
|
|
1263
|
+
rule "specific_sensor_alert" do
|
|
1264
|
+
on :sensor, id: 42, status: "active" # Very selective
|
|
1265
|
+
on :temperature, sensor_id: 42, value: greater_than(80)
|
|
1266
|
+
perform { puts "Alert!" }
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
# Less efficient - Unselective first
|
|
1270
|
+
rule "specific_sensor_alert" do
|
|
1271
|
+
on :temperature, value: greater_than(80) # Matches many facts
|
|
1272
|
+
on :sensor, id: 42, status: "active"
|
|
1273
|
+
perform { puts "Alert!" }
|
|
1274
|
+
end
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
---
|
|
1278
|
+
|
|
1279
|
+
### 4. Use Pattern Helpers
|
|
1280
|
+
|
|
1281
|
+
```ruby
|
|
1282
|
+
# Good - Readable
|
|
1283
|
+
on :temperature, value: between(70, 90)
|
|
1284
|
+
on :order, status: one_of("pending", "processing")
|
|
1285
|
+
|
|
1286
|
+
# Less readable
|
|
1287
|
+
on :temperature, value: ->(v) { v >= 70 && v <= 90 }
|
|
1288
|
+
on :order, status: ->(s) { ["pending", "processing"].include?(s) }
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
---
|
|
1292
|
+
|
|
1293
|
+
### 5. Keep Actions Simple
|
|
1294
|
+
|
|
1295
|
+
```ruby
|
|
1296
|
+
# Good - Simple, focused action
|
|
1297
|
+
rule "log_temperature" do
|
|
1298
|
+
on :temperature, value: :temp?
|
|
1299
|
+
perform { |b| logger.info("Temperature: #{b[:temp?]}") }
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
# Avoid - Complex logic in action
|
|
1303
|
+
rule "complex_action" do
|
|
1304
|
+
on :temperature, value: :temp?
|
|
1305
|
+
perform do |b|
|
|
1306
|
+
# 100 lines of complex logic...
|
|
1307
|
+
# Better to extract to methods
|
|
1308
|
+
end
|
|
1309
|
+
end
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
---
|
|
1313
|
+
|
|
1314
|
+
## See Also
|
|
1315
|
+
|
|
1316
|
+
- [Getting Started Guide](getting-started.md) - First tutorial
|
|
1317
|
+
- [Writing Rules Guide](writing-rules.md) - Best practices for rules
|
|
1318
|
+
- [Pattern Matching Guide](pattern-matching.md) - Advanced pattern matching
|
|
1319
|
+
- [Variable Binding Guide](variable-binding.md) - Join tests and bindings
|
|
1320
|
+
- [Negation Guide](negation.md) - Negation semantics
|
|
1321
|
+
- [Rules API](../api/rules.md) - Programmatic rule creation
|