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,1031 @@
|
|
|
1
|
+
# Expert Systems
|
|
2
|
+
|
|
3
|
+
Build diagnostic expert systems using KBS with knowledge representation, inference engines, explanation facilities, and confidence factors.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
This example demonstrates a medical diagnostic system with:
|
|
8
|
+
|
|
9
|
+
- **Knowledge Base** - Medical symptoms and disease rules
|
|
10
|
+
- **Inference Engine** - Forward and backward chaining
|
|
11
|
+
- **Explanation Facility** - Justification for diagnoses
|
|
12
|
+
- **Confidence Factors** - Probabilistic reasoning
|
|
13
|
+
- **User Interface** - Interactive consultation
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Patient Symptoms → Symptom Analysis → Disease Hypotheses → Diagnosis
|
|
19
|
+
↓ ↓ ↓
|
|
20
|
+
Working Memory Confidence Scores Explanation
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Complete Implementation
|
|
24
|
+
|
|
25
|
+
### Medical Diagnosis System
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'kbs'
|
|
29
|
+
|
|
30
|
+
class MedicalExpertSystem
|
|
31
|
+
def initialize(db_path: 'medical.db')
|
|
32
|
+
@engine = KBS::Blackboard::Engine.new(db_path: db_path)
|
|
33
|
+
@explanations = []
|
|
34
|
+
setup_knowledge_base
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def setup_knowledge_base
|
|
38
|
+
# Rule 1: Detect fever pattern
|
|
39
|
+
fever_rule = KBS::Rule.new("detect_fever", priority: 100) do |r|
|
|
40
|
+
r.conditions = [
|
|
41
|
+
KBS::Condition.new(:symptom, {
|
|
42
|
+
type: "temperature",
|
|
43
|
+
value: :temp?
|
|
44
|
+
}, predicate: lambda { |f| f[:value] > 38.0 }),
|
|
45
|
+
|
|
46
|
+
KBS::Condition.new(:fever_detected, {}, negated: true)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
r.action = lambda do |facts, bindings|
|
|
50
|
+
confidence = calculate_fever_confidence(bindings[:temp?])
|
|
51
|
+
|
|
52
|
+
@engine.add_fact(:fever_detected, {
|
|
53
|
+
severity: fever_severity(bindings[:temp?]),
|
|
54
|
+
confidence: confidence,
|
|
55
|
+
temperature: bindings[:temp?]
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
@explanations << {
|
|
59
|
+
rule: "detect_fever",
|
|
60
|
+
reasoning: "Temperature #{bindings[:temp?]}°C exceeds normal (37°C)",
|
|
61
|
+
confidence: confidence
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Rule 2: Flu hypothesis
|
|
67
|
+
flu_rule = KBS::Rule.new("hypothesize_flu", priority: 90) do |r|
|
|
68
|
+
r.conditions = [
|
|
69
|
+
KBS::Condition.new(:fever_detected, { severity: :severity? }),
|
|
70
|
+
|
|
71
|
+
KBS::Condition.new(:symptom, {
|
|
72
|
+
type: "body_aches",
|
|
73
|
+
present: true
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
KBS::Condition.new(:symptom, {
|
|
77
|
+
type: "fatigue",
|
|
78
|
+
present: true
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
KBS::Condition.new(:diagnosis, { disease: "flu" }, negated: true)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
r.action = lambda do |facts, bindings|
|
|
85
|
+
# Calculate confidence based on symptom presence
|
|
86
|
+
base_confidence = 0.6
|
|
87
|
+
|
|
88
|
+
# Adjust for fever severity
|
|
89
|
+
fever_bonus = bindings[:severity?] == "high" ? 0.2 : 0.1
|
|
90
|
+
|
|
91
|
+
# Check for additional symptoms
|
|
92
|
+
cough = @engine.facts.any? { |f|
|
|
93
|
+
f.type == :symptom && f[:type] == "cough" && f[:present]
|
|
94
|
+
}
|
|
95
|
+
cough_bonus = cough ? 0.1 : 0.0
|
|
96
|
+
|
|
97
|
+
confidence = [base_confidence + fever_bonus + cough_bonus, 1.0].min
|
|
98
|
+
|
|
99
|
+
@engine.add_fact(:diagnosis, {
|
|
100
|
+
disease: "flu",
|
|
101
|
+
confidence: confidence,
|
|
102
|
+
symptoms: ["fever", "body_aches", "fatigue"]
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
@explanations << {
|
|
106
|
+
rule: "hypothesize_flu",
|
|
107
|
+
reasoning: "Classic flu triad: fever + body aches + fatigue",
|
|
108
|
+
confidence: confidence
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Rule 3: Strep throat hypothesis
|
|
114
|
+
strep_rule = KBS::Rule.new("hypothesize_strep_throat", priority: 90) do |r|
|
|
115
|
+
r.conditions = [
|
|
116
|
+
KBS::Condition.new(:symptom, {
|
|
117
|
+
type: "sore_throat",
|
|
118
|
+
severity: :throat_severity?
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
KBS::Condition.new(:symptom, {
|
|
122
|
+
type: "swollen_lymph_nodes",
|
|
123
|
+
present: true
|
|
124
|
+
}),
|
|
125
|
+
|
|
126
|
+
KBS::Condition.new(:fever_detected, {}),
|
|
127
|
+
|
|
128
|
+
# No cough (distinguishes from viral)
|
|
129
|
+
KBS::Condition.new(:symptom, {
|
|
130
|
+
type: "cough",
|
|
131
|
+
present: true
|
|
132
|
+
}, negated: true),
|
|
133
|
+
|
|
134
|
+
KBS::Condition.new(:diagnosis, { disease: "strep_throat" }, negated: true)
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
r.action = lambda do |facts, bindings|
|
|
138
|
+
base_confidence = 0.7
|
|
139
|
+
|
|
140
|
+
# Severe sore throat increases confidence
|
|
141
|
+
severity_bonus = bindings[:throat_severity?] == "severe" ? 0.2 : 0.1
|
|
142
|
+
|
|
143
|
+
confidence = [base_confidence + severity_bonus, 0.95].min
|
|
144
|
+
|
|
145
|
+
@engine.add_fact(:diagnosis, {
|
|
146
|
+
disease: "strep_throat",
|
|
147
|
+
confidence: confidence,
|
|
148
|
+
symptoms: ["sore_throat", "swollen_lymph_nodes", "fever", "no_cough"]
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
@explanations << {
|
|
152
|
+
rule: "hypothesize_strep_throat",
|
|
153
|
+
reasoning: "Sore throat + swollen nodes + fever WITHOUT cough suggests bacterial infection",
|
|
154
|
+
confidence: confidence
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Rule 4: Common cold hypothesis
|
|
160
|
+
cold_rule = KBS::Rule.new("hypothesize_cold", priority: 85) do |r|
|
|
161
|
+
r.conditions = [
|
|
162
|
+
KBS::Condition.new(:symptom, {
|
|
163
|
+
type: "runny_nose",
|
|
164
|
+
present: true
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
KBS::Condition.new(:symptom, {
|
|
168
|
+
type: "sneezing",
|
|
169
|
+
present: true
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
KBS::Condition.new(:symptom, {
|
|
173
|
+
type: "congestion",
|
|
174
|
+
present: true
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
# Mild or no fever
|
|
178
|
+
KBS::Condition.new(:fever_detected, {
|
|
179
|
+
severity: "high"
|
|
180
|
+
}, negated: true),
|
|
181
|
+
|
|
182
|
+
KBS::Condition.new(:diagnosis, { disease: "common_cold" }, negated: true)
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
r.action = lambda do |facts, bindings|
|
|
186
|
+
confidence = 0.75
|
|
187
|
+
|
|
188
|
+
# Adjust if low fever present
|
|
189
|
+
low_fever = @engine.facts.any? { |f|
|
|
190
|
+
f.type == :fever_detected && f[:severity] == "low"
|
|
191
|
+
}
|
|
192
|
+
confidence += 0.1 if low_fever
|
|
193
|
+
|
|
194
|
+
@engine.add_fact(:diagnosis, {
|
|
195
|
+
disease: "common_cold",
|
|
196
|
+
confidence: confidence,
|
|
197
|
+
symptoms: ["runny_nose", "sneezing", "congestion"]
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
@explanations << {
|
|
201
|
+
rule: "hypothesize_cold",
|
|
202
|
+
reasoning: "Upper respiratory symptoms without high fever typical of viral cold",
|
|
203
|
+
confidence: confidence
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Rule 5: Allergy hypothesis
|
|
209
|
+
allergy_rule = KBS::Rule.new("hypothesize_allergy", priority: 85) do |r|
|
|
210
|
+
r.conditions = [
|
|
211
|
+
KBS::Condition.new(:symptom, {
|
|
212
|
+
type: "sneezing",
|
|
213
|
+
frequency: :freq?
|
|
214
|
+
}, predicate: lambda { |f| f[:frequency] == "frequent" }),
|
|
215
|
+
|
|
216
|
+
KBS::Condition.new(:symptom, {
|
|
217
|
+
type: "itchy_eyes",
|
|
218
|
+
present: true
|
|
219
|
+
}),
|
|
220
|
+
|
|
221
|
+
KBS::Condition.new(:symptom, {
|
|
222
|
+
type: "runny_nose",
|
|
223
|
+
present: true
|
|
224
|
+
}),
|
|
225
|
+
|
|
226
|
+
# No fever (key differentiator from infection)
|
|
227
|
+
KBS::Condition.new(:fever_detected, {}, negated: true),
|
|
228
|
+
|
|
229
|
+
KBS::Condition.new(:diagnosis, { disease: "allergies" }, negated: true)
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
r.action = lambda do |facts, bindings|
|
|
233
|
+
confidence = 0.8
|
|
234
|
+
|
|
235
|
+
@engine.add_fact(:diagnosis, {
|
|
236
|
+
disease: "allergies",
|
|
237
|
+
confidence: confidence,
|
|
238
|
+
symptoms: ["frequent_sneezing", "itchy_eyes", "runny_nose", "no_fever"]
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
@explanations << {
|
|
242
|
+
rule: "hypothesize_allergy",
|
|
243
|
+
reasoning: "Frequent sneezing + itchy eyes + runny nose WITHOUT fever suggests allergic reaction",
|
|
244
|
+
confidence: confidence
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Rule 6: Migraine hypothesis
|
|
250
|
+
migraine_rule = KBS::Rule.new("hypothesize_migraine", priority: 88) do |r|
|
|
251
|
+
r.conditions = [
|
|
252
|
+
KBS::Condition.new(:symptom, {
|
|
253
|
+
type: "headache",
|
|
254
|
+
location: "unilateral",
|
|
255
|
+
severity: :severity?
|
|
256
|
+
}, predicate: lambda { |f| f[:severity] == "severe" }),
|
|
257
|
+
|
|
258
|
+
KBS::Condition.new(:symptom, {
|
|
259
|
+
type: "nausea",
|
|
260
|
+
present: true
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
KBS::Condition.new(:symptom, {
|
|
264
|
+
type: "light_sensitivity",
|
|
265
|
+
present: true
|
|
266
|
+
}),
|
|
267
|
+
|
|
268
|
+
KBS::Condition.new(:diagnosis, { disease: "migraine" }, negated: true)
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
r.action = lambda do |facts, bindings|
|
|
272
|
+
base_confidence = 0.85
|
|
273
|
+
|
|
274
|
+
# Check for aura
|
|
275
|
+
aura = @engine.facts.any? { |f|
|
|
276
|
+
f.type == :symptom && f[:type] == "visual_disturbance"
|
|
277
|
+
}
|
|
278
|
+
aura_bonus = aura ? 0.1 : 0.0
|
|
279
|
+
|
|
280
|
+
confidence = [base_confidence + aura_bonus, 0.95].min
|
|
281
|
+
|
|
282
|
+
@engine.add_fact(:diagnosis, {
|
|
283
|
+
disease: "migraine",
|
|
284
|
+
confidence: confidence,
|
|
285
|
+
symptoms: ["severe_unilateral_headache", "nausea", "photophobia"]
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
@explanations << {
|
|
289
|
+
rule: "hypothesize_migraine",
|
|
290
|
+
reasoning: "Severe one-sided headache with nausea and light sensitivity characteristic of migraine",
|
|
291
|
+
confidence: confidence
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Rule 7: Recommend diagnostic test
|
|
297
|
+
test_rule = KBS::Rule.new("recommend_diagnostic_test", priority: 70) do |r|
|
|
298
|
+
r.conditions = [
|
|
299
|
+
KBS::Condition.new(:diagnosis, {
|
|
300
|
+
disease: :disease?,
|
|
301
|
+
confidence: :conf?
|
|
302
|
+
}, predicate: lambda { |f| f[:confidence] > 0.7 && f[:confidence] < 0.9 }),
|
|
303
|
+
|
|
304
|
+
KBS::Condition.new(:test_recommended, {
|
|
305
|
+
disease: :disease?
|
|
306
|
+
}, negated: true)
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
r.action = lambda do |facts, bindings|
|
|
310
|
+
test = diagnostic_test_for(bindings[:disease?])
|
|
311
|
+
|
|
312
|
+
@engine.add_fact(:test_recommended, {
|
|
313
|
+
disease: bindings[:disease?],
|
|
314
|
+
test: test,
|
|
315
|
+
reason: "Confidence #{bindings[:conf?]} warrants confirmation"
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
@explanations << {
|
|
319
|
+
rule: "recommend_diagnostic_test",
|
|
320
|
+
reasoning: "Moderate confidence (#{bindings[:conf?]}) suggests #{test} for confirmation",
|
|
321
|
+
confidence: 1.0
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Rule 8: Final diagnosis
|
|
327
|
+
final_diagnosis_rule = KBS::Rule.new("select_final_diagnosis", priority: 60) do |r|
|
|
328
|
+
r.conditions = [
|
|
329
|
+
KBS::Condition.new(:diagnosis, {
|
|
330
|
+
disease: :disease?,
|
|
331
|
+
confidence: :conf?
|
|
332
|
+
}),
|
|
333
|
+
|
|
334
|
+
KBS::Condition.new(:final_diagnosis, {}, negated: true)
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
r.action = lambda do |facts, bindings|
|
|
338
|
+
# Find highest confidence diagnosis
|
|
339
|
+
all_diagnoses = facts.select { |f| f.type == :diagnosis }
|
|
340
|
+
best = all_diagnoses.max_by { |d| d[:confidence] }
|
|
341
|
+
|
|
342
|
+
@engine.add_fact(:final_diagnosis, {
|
|
343
|
+
disease: best[:disease],
|
|
344
|
+
confidence: best[:confidence],
|
|
345
|
+
symptoms: best[:symptoms],
|
|
346
|
+
timestamp: Time.now
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
@explanations << {
|
|
350
|
+
rule: "select_final_diagnosis",
|
|
351
|
+
reasoning: "Selected #{best[:disease]} (#{best[:confidence]} confidence) as most likely diagnosis",
|
|
352
|
+
confidence: 1.0
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
@engine.add_rule(fever_rule)
|
|
358
|
+
@engine.add_rule(flu_rule)
|
|
359
|
+
@engine.add_rule(strep_rule)
|
|
360
|
+
@engine.add_rule(cold_rule)
|
|
361
|
+
@engine.add_rule(allergy_rule)
|
|
362
|
+
@engine.add_rule(migraine_rule)
|
|
363
|
+
@engine.add_rule(test_rule)
|
|
364
|
+
@engine.add_rule(final_diagnosis_rule)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def add_symptom(type, attributes = {})
|
|
368
|
+
@engine.add_fact(:symptom, { type: type, **attributes })
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def diagnose
|
|
372
|
+
@explanations.clear
|
|
373
|
+
@engine.run
|
|
374
|
+
|
|
375
|
+
final = @engine.facts.find { |f| f.type == :final_diagnosis }
|
|
376
|
+
|
|
377
|
+
{
|
|
378
|
+
diagnosis: final,
|
|
379
|
+
all_hypotheses: @engine.facts.select { |f| f.type == :diagnosis },
|
|
380
|
+
explanations: @explanations,
|
|
381
|
+
recommended_tests: @engine.facts.select { |f| f.type == :test_recommended }
|
|
382
|
+
}
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def explain_reasoning
|
|
386
|
+
@explanations.each_with_index do |exp, i|
|
|
387
|
+
puts "\n#{i + 1}. Rule: #{exp[:rule]} (Confidence: #{exp[:confidence]})"
|
|
388
|
+
puts " Reasoning: #{exp[:reasoning]}"
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
def calculate_fever_confidence(temp)
|
|
395
|
+
case temp
|
|
396
|
+
when 38.0..38.5
|
|
397
|
+
0.6
|
|
398
|
+
when 38.5..39.0
|
|
399
|
+
0.75
|
|
400
|
+
when 39.0..40.0
|
|
401
|
+
0.9
|
|
402
|
+
else
|
|
403
|
+
0.95
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def fever_severity(temp)
|
|
408
|
+
case temp
|
|
409
|
+
when 38.0..38.5
|
|
410
|
+
"low"
|
|
411
|
+
when 38.5..39.5
|
|
412
|
+
"moderate"
|
|
413
|
+
else
|
|
414
|
+
"high"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def diagnostic_test_for(disease)
|
|
419
|
+
{
|
|
420
|
+
"flu" => "Rapid influenza test",
|
|
421
|
+
"strep_throat" => "Rapid strep test (throat swab)",
|
|
422
|
+
"migraine" => "MRI (if first occurrence or atypical presentation)",
|
|
423
|
+
"common_cold" => "None (clinical diagnosis)",
|
|
424
|
+
"allergies" => "Allergy skin test or IgE blood test"
|
|
425
|
+
}[disease] || "Consult physician"
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Usage Example 1: Flu Diagnosis
|
|
430
|
+
puts "=== Example 1: Flu Diagnosis ==="
|
|
431
|
+
system = MedicalExpertSystem.new(db_path: ':memory:')
|
|
432
|
+
|
|
433
|
+
system.add_symptom("temperature", value: 39.2)
|
|
434
|
+
system.add_symptom("body_aches", present: true)
|
|
435
|
+
system.add_symptom("fatigue", present: true)
|
|
436
|
+
system.add_symptom("cough", present: true)
|
|
437
|
+
|
|
438
|
+
result = system.diagnose
|
|
439
|
+
|
|
440
|
+
puts "\nFinal Diagnosis:"
|
|
441
|
+
if result[:diagnosis]
|
|
442
|
+
puts " Disease: #{result[:diagnosis][:disease]}"
|
|
443
|
+
puts " Confidence: #{(result[:diagnosis][:confidence] * 100).round(1)}%"
|
|
444
|
+
puts " Symptoms: #{result[:diagnosis][:symptoms].join(', ')}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
puts "\nAll Hypotheses:"
|
|
448
|
+
result[:all_hypotheses].each do |h|
|
|
449
|
+
puts " - #{h[:disease]}: #{(h[:confidence] * 100).round(1)}%"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
puts "\nReasoning Chain:"
|
|
453
|
+
system.explain_reasoning
|
|
454
|
+
|
|
455
|
+
# Usage Example 2: Strep Throat
|
|
456
|
+
puts "\n\n=== Example 2: Strep Throat Diagnosis ==="
|
|
457
|
+
system2 = MedicalExpertSystem.new(db_path: ':memory:')
|
|
458
|
+
|
|
459
|
+
system2.add_symptom("temperature", value: 38.8)
|
|
460
|
+
system2.add_symptom("sore_throat", severity: "severe", present: true)
|
|
461
|
+
system2.add_symptom("swollen_lymph_nodes", present: true)
|
|
462
|
+
# Note: No cough symptom added
|
|
463
|
+
|
|
464
|
+
result2 = system2.diagnose
|
|
465
|
+
|
|
466
|
+
puts "\nFinal Diagnosis:"
|
|
467
|
+
if result2[:diagnosis]
|
|
468
|
+
puts " Disease: #{result2[:diagnosis][:disease]}"
|
|
469
|
+
puts " Confidence: #{(result2[:diagnosis][:confidence] * 100).round(1)}%"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
if result2[:recommended_tests].any?
|
|
473
|
+
puts "\nRecommended Tests:"
|
|
474
|
+
result2[:recommended_tests].each do |test|
|
|
475
|
+
puts " - #{test[:test]} for #{test[:disease]}"
|
|
476
|
+
puts " Reason: #{test[:reason]}"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
puts "\nReasoning Chain:"
|
|
481
|
+
system2.explain_reasoning
|
|
482
|
+
|
|
483
|
+
# Usage Example 3: Allergies
|
|
484
|
+
puts "\n\n=== Example 3: Allergy Diagnosis ==="
|
|
485
|
+
system3 = MedicalExpertSystem.new(db_path: ':memory:')
|
|
486
|
+
|
|
487
|
+
system3.add_symptom("sneezing", frequency: "frequent", present: true)
|
|
488
|
+
system3.add_symptom("itchy_eyes", present: true)
|
|
489
|
+
system3.add_symptom("runny_nose", present: true)
|
|
490
|
+
system3.add_symptom("congestion", present: true)
|
|
491
|
+
# Note: No fever
|
|
492
|
+
|
|
493
|
+
result3 = system3.diagnose
|
|
494
|
+
|
|
495
|
+
puts "\nFinal Diagnosis:"
|
|
496
|
+
if result3[:diagnosis]
|
|
497
|
+
puts " Disease: #{result3[:diagnosis][:disease]}"
|
|
498
|
+
puts " Confidence: #{(result3[:diagnosis][:confidence] * 100).round(1)}%"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
puts "\nReasoning Chain:"
|
|
502
|
+
system3.explain_reasoning
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Key Features
|
|
506
|
+
|
|
507
|
+
### 1. Knowledge Representation
|
|
508
|
+
|
|
509
|
+
Rules encode medical knowledge in a structured format:
|
|
510
|
+
|
|
511
|
+
```ruby
|
|
512
|
+
# Rule encodes: "IF fever AND body_aches AND fatigue THEN possibly flu"
|
|
513
|
+
KBS::Rule.new("hypothesize_flu") do |r|
|
|
514
|
+
r.conditions = [
|
|
515
|
+
KBS::Condition.new(:fever_detected, { severity: :severity? }),
|
|
516
|
+
KBS::Condition.new(:symptom, { type: "body_aches", present: true }),
|
|
517
|
+
KBS::Condition.new(:symptom, { type: "fatigue", present: true })
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
r.action = lambda do |facts, bindings|
|
|
521
|
+
# Calculate confidence and add diagnosis
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 2. Confidence Factors
|
|
527
|
+
|
|
528
|
+
Probabilistic reasoning using confidence scores:
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
base_confidence = 0.6
|
|
532
|
+
fever_bonus = bindings[:severity?] == "high" ? 0.2 : 0.1
|
|
533
|
+
cough_bonus = cough_present? ? 0.1 : 0.0
|
|
534
|
+
|
|
535
|
+
confidence = [base_confidence + fever_bonus + cough_bonus, 1.0].min
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### 3. Explanation Facility
|
|
539
|
+
|
|
540
|
+
Track reasoning for transparency:
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
@explanations << {
|
|
544
|
+
rule: "hypothesize_flu",
|
|
545
|
+
reasoning: "Classic flu triad: fever + body aches + fatigue",
|
|
546
|
+
confidence: confidence
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### 4. Differential Diagnosis
|
|
551
|
+
|
|
552
|
+
Multiple hypotheses with varying confidence:
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
# System can maintain:
|
|
556
|
+
# - Flu: 85% confidence
|
|
557
|
+
# - Common cold: 60% confidence
|
|
558
|
+
# - Strep throat: 40% confidence
|
|
559
|
+
|
|
560
|
+
all_diagnoses = facts.select { |f| f.type == :diagnosis }
|
|
561
|
+
best = all_diagnoses.max_by { |d| d[:confidence] }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### 5. Negation for Diagnosis
|
|
565
|
+
|
|
566
|
+
Use absence of symptoms to refine diagnosis:
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
569
|
+
# Strep throat: sore throat + fever WITHOUT cough
|
|
570
|
+
KBS::Condition.new(:symptom, {
|
|
571
|
+
type: "cough",
|
|
572
|
+
present: true
|
|
573
|
+
}, negated: true)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Expert System Patterns
|
|
577
|
+
|
|
578
|
+
### Forward Chaining
|
|
579
|
+
|
|
580
|
+
Data-driven reasoning from symptoms to diagnosis:
|
|
581
|
+
|
|
582
|
+
```
|
|
583
|
+
Symptoms → Intermediate Facts → Hypotheses → Final Diagnosis
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
# 1. Symptom facts added
|
|
588
|
+
add_symptom("temperature", value: 39.2)
|
|
589
|
+
|
|
590
|
+
# 2. Engine detects fever
|
|
591
|
+
fever_detected fact created
|
|
592
|
+
|
|
593
|
+
# 3. Engine hypothesizes diseases
|
|
594
|
+
diagnosis facts created
|
|
595
|
+
|
|
596
|
+
# 4. Engine selects best diagnosis
|
|
597
|
+
final_diagnosis fact created
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Backward Chaining
|
|
601
|
+
|
|
602
|
+
Goal-driven reasoning (query mode):
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
class BackwardChainingExpert < MedicalExpertSystem
|
|
606
|
+
def why_diagnosis?(disease)
|
|
607
|
+
diagnosis = @engine.facts.find { |f|
|
|
608
|
+
f.type == :diagnosis && f[:disease] == disease
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return nil unless diagnosis
|
|
612
|
+
|
|
613
|
+
# Find which symptoms led to this diagnosis
|
|
614
|
+
required_symptoms = diagnosis[:symptoms]
|
|
615
|
+
present_symptoms = @engine.facts.select { |f|
|
|
616
|
+
f.type == :symptom && required_symptoms.include?(f[:type])
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
{
|
|
620
|
+
disease: disease,
|
|
621
|
+
confidence: diagnosis[:confidence],
|
|
622
|
+
supporting_symptoms: present_symptoms,
|
|
623
|
+
reasoning: @explanations.find { |e| e[:rule].include?(disease) }
|
|
624
|
+
}
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Usage
|
|
629
|
+
expert = BackwardChainingExpert.new
|
|
630
|
+
expert.add_symptom("fever", value: 39.0)
|
|
631
|
+
expert.add_symptom("body_aches", present: true)
|
|
632
|
+
expert.diagnose
|
|
633
|
+
|
|
634
|
+
why = expert.why_diagnosis?("flu")
|
|
635
|
+
puts "Why flu?"
|
|
636
|
+
puts " Confidence: #{why[:confidence]}"
|
|
637
|
+
puts " Supporting: #{why[:supporting_symptoms].map(&:type).join(', ')}"
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Certainty Factors
|
|
641
|
+
|
|
642
|
+
Combine evidence with certainty calculus:
|
|
643
|
+
|
|
644
|
+
```ruby
|
|
645
|
+
def combine_certainty_factors(cf1, cf2)
|
|
646
|
+
if cf1 > 0 && cf2 > 0
|
|
647
|
+
cf1 + cf2 * (1 - cf1)
|
|
648
|
+
elsif cf1 < 0 && cf2 < 0
|
|
649
|
+
cf1 + cf2 * (1 + cf1)
|
|
650
|
+
else
|
|
651
|
+
(cf1 + cf2) / (1 - [cf1.abs, cf2.abs].min)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Example: Multiple pieces of evidence for flu
|
|
656
|
+
fever_cf = 0.6
|
|
657
|
+
aches_cf = 0.4
|
|
658
|
+
cough_cf = 0.3
|
|
659
|
+
|
|
660
|
+
combined = combine_certainty_factors(fever_cf, aches_cf)
|
|
661
|
+
combined = combine_certainty_factors(combined, cough_cf)
|
|
662
|
+
# Result: Higher confidence with more evidence
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Meta-Rules
|
|
666
|
+
|
|
667
|
+
Rules about rules:
|
|
668
|
+
|
|
669
|
+
```ruby
|
|
670
|
+
# Meta-rule: If confidence moderate, recommend test
|
|
671
|
+
KBS::Rule.new("recommend_test_meta", priority: 50) do |r|
|
|
672
|
+
r.conditions = [
|
|
673
|
+
KBS::Condition.new(:diagnosis, {
|
|
674
|
+
confidence: :conf?
|
|
675
|
+
}, predicate: lambda { |f| f[:confidence].between?(0.5, 0.85) })
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
r.action = lambda do |facts, bindings|
|
|
679
|
+
@engine.add_fact(:action_needed, {
|
|
680
|
+
type: "diagnostic_test",
|
|
681
|
+
reason: "Confidence not high enough for treatment"
|
|
682
|
+
})
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
## Advanced Features
|
|
688
|
+
|
|
689
|
+
### Temporal Reasoning
|
|
690
|
+
|
|
691
|
+
Track symptom progression:
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
class TemporalExpertSystem < MedicalExpertSystem
|
|
695
|
+
def add_symptom_with_timing(type, onset, attributes = {})
|
|
696
|
+
@engine.add_fact(:symptom, {
|
|
697
|
+
type: type,
|
|
698
|
+
onset: onset,
|
|
699
|
+
**attributes
|
|
700
|
+
})
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def setup_temporal_rules
|
|
704
|
+
# Rule: Rapid onset fever + headache suggests infection
|
|
705
|
+
rapid_onset_rule = KBS::Rule.new("rapid_onset_infection") do |r|
|
|
706
|
+
r.conditions = [
|
|
707
|
+
KBS::Condition.new(:symptom, {
|
|
708
|
+
type: "fever",
|
|
709
|
+
onset: :onset1?
|
|
710
|
+
}, predicate: lambda { |f|
|
|
711
|
+
(Time.now - f[:onset]) < 3600 * 24 # < 24 hours
|
|
712
|
+
}),
|
|
713
|
+
|
|
714
|
+
KBS::Condition.new(:symptom, {
|
|
715
|
+
type: "headache",
|
|
716
|
+
onset: :onset2?
|
|
717
|
+
}, predicate: lambda { |f|
|
|
718
|
+
(Time.now - f[:onset]) < 3600 * 24
|
|
719
|
+
})
|
|
720
|
+
]
|
|
721
|
+
|
|
722
|
+
r.action = lambda do |facts, bindings|
|
|
723
|
+
@engine.add_fact(:diagnosis, {
|
|
724
|
+
disease: "acute_infection",
|
|
725
|
+
confidence: 0.75,
|
|
726
|
+
reasoning: "Rapid onset suggests acute process"
|
|
727
|
+
})
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
@engine.add_rule(rapid_onset_rule)
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Conflict Resolution
|
|
737
|
+
|
|
738
|
+
Handle contradictory evidence:
|
|
739
|
+
|
|
740
|
+
```ruby
|
|
741
|
+
# Rule: Resolve conflicting diagnoses
|
|
742
|
+
KBS::Rule.new("resolve_conflicts", priority: 55) do |r|
|
|
743
|
+
r.conditions = [
|
|
744
|
+
KBS::Condition.new(:diagnosis, {
|
|
745
|
+
disease: "flu",
|
|
746
|
+
confidence: :flu_conf?
|
|
747
|
+
}),
|
|
748
|
+
|
|
749
|
+
KBS::Condition.new(:diagnosis, {
|
|
750
|
+
disease: "common_cold",
|
|
751
|
+
confidence: :cold_conf?
|
|
752
|
+
})
|
|
753
|
+
]
|
|
754
|
+
|
|
755
|
+
r.action = lambda do |facts, bindings|
|
|
756
|
+
# Flu and cold are mutually exclusive
|
|
757
|
+
if bindings[:flu_conf?] > bindings[:cold_conf?]
|
|
758
|
+
cold = facts.find { |f| f.type == :diagnosis && f[:disease] == "common_cold" }
|
|
759
|
+
@engine.remove_fact(cold)
|
|
760
|
+
else
|
|
761
|
+
flu = facts.find { |f| f.type == :diagnosis && f[:disease] == "flu" }
|
|
762
|
+
@engine.remove_fact(flu)
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Learning from Cases
|
|
769
|
+
|
|
770
|
+
Update confidence factors based on outcomes:
|
|
771
|
+
|
|
772
|
+
```ruby
|
|
773
|
+
class LearningExpertSystem < MedicalExpertSystem
|
|
774
|
+
def initialize(db_path: 'medical_learning.db')
|
|
775
|
+
super
|
|
776
|
+
@case_history = load_case_history
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def record_outcome(symptoms, actual_diagnosis)
|
|
780
|
+
# Store case for learning
|
|
781
|
+
@case_history << {
|
|
782
|
+
symptoms: symptoms,
|
|
783
|
+
diagnosis: actual_diagnosis,
|
|
784
|
+
timestamp: Time.now
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
save_case_history
|
|
788
|
+
update_confidence_weights
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def update_confidence_weights
|
|
792
|
+
# Analyze historical accuracy
|
|
793
|
+
# Adjust confidence factors for rules
|
|
794
|
+
@case_history.group_by { |c| c[:diagnosis] }.each do |disease, cases|
|
|
795
|
+
accuracy = calculate_accuracy(disease, cases)
|
|
796
|
+
adjust_rule_confidence(disease, accuracy)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
private
|
|
801
|
+
|
|
802
|
+
def calculate_accuracy(disease, cases)
|
|
803
|
+
# Calculate how often diagnosis was correct
|
|
804
|
+
cases.count { |c| c[:confirmed] }.to_f / cases.size
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def adjust_rule_confidence(disease, accuracy)
|
|
808
|
+
# Modify confidence factors based on historical performance
|
|
809
|
+
# Implementation depends on your confidence model
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
## Testing
|
|
815
|
+
|
|
816
|
+
```ruby
|
|
817
|
+
require 'minitest/autorun'
|
|
818
|
+
|
|
819
|
+
class TestMedicalExpertSystem < Minitest::Test
|
|
820
|
+
def setup
|
|
821
|
+
@system = MedicalExpertSystem.new(db_path: ':memory:')
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def test_flu_diagnosis
|
|
825
|
+
@system.add_symptom("temperature", value: 39.0)
|
|
826
|
+
@system.add_symptom("body_aches", present: true)
|
|
827
|
+
@system.add_symptom("fatigue", present: true)
|
|
828
|
+
|
|
829
|
+
result = @system.diagnose
|
|
830
|
+
|
|
831
|
+
assert_equal "flu", result[:diagnosis][:disease]
|
|
832
|
+
assert result[:diagnosis][:confidence] > 0.6
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def test_strep_throat_vs_viral
|
|
836
|
+
@system.add_symptom("temperature", value: 38.8)
|
|
837
|
+
@system.add_symptom("sore_throat", severity: "severe", present: true)
|
|
838
|
+
@system.add_symptom("swollen_lymph_nodes", present: true)
|
|
839
|
+
# No cough - key differentiator
|
|
840
|
+
|
|
841
|
+
result = @system.diagnose
|
|
842
|
+
|
|
843
|
+
assert_equal "strep_throat", result[:diagnosis][:disease]
|
|
844
|
+
assert result[:diagnosis][:confidence] > 0.7
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def test_allergy_no_fever
|
|
848
|
+
@system.add_symptom("sneezing", frequency: "frequent", present: true)
|
|
849
|
+
@system.add_symptom("itchy_eyes", present: true)
|
|
850
|
+
@system.add_symptom("runny_nose", present: true)
|
|
851
|
+
|
|
852
|
+
result = @system.diagnose
|
|
853
|
+
|
|
854
|
+
assert_equal "allergies", result[:diagnosis][:disease]
|
|
855
|
+
|
|
856
|
+
# Should NOT detect fever
|
|
857
|
+
fever_facts = @system.instance_variable_get(:@engine).facts.select { |f|
|
|
858
|
+
f.type == :fever_detected
|
|
859
|
+
}
|
|
860
|
+
assert_empty fever_facts
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def test_differential_diagnosis
|
|
864
|
+
@system.add_symptom("temperature", value: 38.2)
|
|
865
|
+
@system.add_symptom("runny_nose", present: true)
|
|
866
|
+
@system.add_symptom("congestion", present: true)
|
|
867
|
+
@system.add_symptom("sneezing", present: true)
|
|
868
|
+
|
|
869
|
+
result = @system.diagnose
|
|
870
|
+
|
|
871
|
+
# Should have multiple hypotheses
|
|
872
|
+
assert result[:all_hypotheses].size > 1
|
|
873
|
+
|
|
874
|
+
# Cold should win (has all symptoms)
|
|
875
|
+
assert_equal "common_cold", result[:diagnosis][:disease]
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def test_confidence_factors
|
|
879
|
+
@system.add_symptom("temperature", value: 40.5) # Very high fever
|
|
880
|
+
@system.add_symptom("body_aches", present: true)
|
|
881
|
+
@system.add_symptom("fatigue", present: true)
|
|
882
|
+
@system.add_symptom("cough", present: true)
|
|
883
|
+
|
|
884
|
+
result = @system.diagnose
|
|
885
|
+
|
|
886
|
+
# High fever + all symptoms = high confidence
|
|
887
|
+
assert result[:diagnosis][:confidence] > 0.8
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def test_explanation_facility
|
|
891
|
+
@system.add_symptom("temperature", value: 39.0)
|
|
892
|
+
@system.add_symptom("body_aches", present: true)
|
|
893
|
+
@system.add_symptom("fatigue", present: true)
|
|
894
|
+
|
|
895
|
+
result = @system.diagnose
|
|
896
|
+
|
|
897
|
+
explanations = result[:explanations]
|
|
898
|
+
|
|
899
|
+
# Should have explanations for each rule fired
|
|
900
|
+
assert explanations.size > 0
|
|
901
|
+
|
|
902
|
+
# Each explanation should have rule, reasoning, confidence
|
|
903
|
+
explanations.each do |exp|
|
|
904
|
+
assert exp[:rule]
|
|
905
|
+
assert exp[:reasoning]
|
|
906
|
+
assert exp[:confidence]
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def test_diagnostic_test_recommendation
|
|
911
|
+
@system.add_symptom("temperature", value: 38.5)
|
|
912
|
+
@system.add_symptom("body_aches", present: true)
|
|
913
|
+
@system.add_symptom("fatigue", present: true)
|
|
914
|
+
# Moderate confidence scenario
|
|
915
|
+
|
|
916
|
+
result = @system.diagnose
|
|
917
|
+
|
|
918
|
+
# Should recommend confirmatory test if confidence moderate
|
|
919
|
+
if result[:diagnosis][:confidence].between?(0.7, 0.9)
|
|
920
|
+
assert result[:recommended_tests].any?
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
## Performance Optimization
|
|
927
|
+
|
|
928
|
+
### Use Blackboard for Complex Cases
|
|
929
|
+
|
|
930
|
+
```ruby
|
|
931
|
+
# For large knowledge bases, use persistent storage
|
|
932
|
+
system = MedicalExpertSystem.new(db_path: 'medical_kb.db')
|
|
933
|
+
|
|
934
|
+
# Facts persist across consultations
|
|
935
|
+
# Audit trail for medical record keeping
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### Index Common Queries
|
|
939
|
+
|
|
940
|
+
```ruby
|
|
941
|
+
class OptimizedExpertSystem < MedicalExpertSystem
|
|
942
|
+
def initialize(db_path:)
|
|
943
|
+
super
|
|
944
|
+
@symptom_index = {}
|
|
945
|
+
@diagnosis_cache = {}
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def add_symptom(type, attributes = {})
|
|
949
|
+
super
|
|
950
|
+
|
|
951
|
+
# Index for fast lookup
|
|
952
|
+
@symptom_index[type] ||= []
|
|
953
|
+
@symptom_index[type] << attributes
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def has_symptom?(type)
|
|
957
|
+
@symptom_index.key?(type)
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
## Production Considerations
|
|
963
|
+
|
|
964
|
+
### Disclaimer and Safety
|
|
965
|
+
|
|
966
|
+
```ruby
|
|
967
|
+
def diagnose
|
|
968
|
+
result = super
|
|
969
|
+
|
|
970
|
+
result[:disclaimer] = "This is an expert system for educational purposes. " \
|
|
971
|
+
"Always consult a qualified healthcare professional " \
|
|
972
|
+
"for medical advice."
|
|
973
|
+
|
|
974
|
+
result
|
|
975
|
+
end
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### Audit Trail
|
|
979
|
+
|
|
980
|
+
```ruby
|
|
981
|
+
# Blackboard automatically logs all reasoning
|
|
982
|
+
system = MedicalExpertSystem.new(db_path: 'medical_audit.db')
|
|
983
|
+
|
|
984
|
+
# Later: Review consultation
|
|
985
|
+
consultation = system.instance_variable_get(:@engine)
|
|
986
|
+
.fact_history
|
|
987
|
+
.select { |h| h[:fact_type] == :diagnosis }
|
|
988
|
+
|
|
989
|
+
puts "Diagnosis history:"
|
|
990
|
+
consultation.each do |entry|
|
|
991
|
+
puts "#{entry[:timestamp]}: #{entry[:attributes][:disease]} (#{entry[:attributes][:confidence]})"
|
|
992
|
+
end
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Integration with Clinical Systems
|
|
996
|
+
|
|
997
|
+
```ruby
|
|
998
|
+
class ClinicalExpertSystem < MedicalExpertSystem
|
|
999
|
+
def import_from_ehr(patient_id, ehr_client)
|
|
1000
|
+
# Import symptoms from Electronic Health Record
|
|
1001
|
+
symptoms = ehr_client.get_symptoms(patient_id)
|
|
1002
|
+
|
|
1003
|
+
symptoms.each do |symptom|
|
|
1004
|
+
add_symptom(symptom[:type], symptom[:attributes])
|
|
1005
|
+
end
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
def export_diagnosis_to_ehr(patient_id, ehr_client)
|
|
1009
|
+
result = diagnose
|
|
1010
|
+
|
|
1011
|
+
ehr_client.add_note(patient_id, {
|
|
1012
|
+
type: "expert_system_consultation",
|
|
1013
|
+
diagnosis: result[:diagnosis],
|
|
1014
|
+
confidence: result[:diagnosis][:confidence],
|
|
1015
|
+
reasoning: result[:explanations],
|
|
1016
|
+
timestamp: Time.now
|
|
1017
|
+
})
|
|
1018
|
+
end
|
|
1019
|
+
end
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
## Next Steps
|
|
1023
|
+
|
|
1024
|
+
- **[Multi-Agent Example](multi-agent.md)** - Multiple expert systems collaborating
|
|
1025
|
+
- **[Blackboard Memory](../guides/blackboard-memory.md)** - Persistent knowledge bases
|
|
1026
|
+
- **[Performance Guide](../advanced/performance.md)** - Optimize large knowledge bases
|
|
1027
|
+
- **[Testing Guide](../advanced/testing.md)** - Test expert system rules
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
*Expert systems encode domain expertise in rules and reasoning. KBS provides the inference engine, you provide the knowledge.*
|