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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. 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.*