File size: 15,525 Bytes
acaf471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
"""
Deterministic rule-based clinical inference engine.

Applies expert-validated If-Then rules to structured pain ontology data.
NO probabilistic reasoning or LLM-based inference - all clinical logic is 
deterministic and traceable.

This module implements the symbolic reasoning component of the neuro-symbolic
hybrid architecture, ensuring medical decisions are explainable and evidence-based.
"""

from typing import List, Dict, Callable
from dataclasses import dataclass
import sys
import os

# Add Backend to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from models.pain_schema import PainOntology, ClinicalRecommendation


@dataclass
class ClinicalRule:
    """
    Represents a single clinical decision rule.
    
    Each rule consists of:
    - Condition: A function that evaluates PainOntology and returns True/False
    - Action: The recommendation to make if condition is met
    - Evidence fields: Which PainOntology fields to include as evidence
    - Guideline reference: Citation to clinical guideline supporting the rule
    """
    rule_id: str
    name: str
    condition: Callable[[PainOntology], bool]
    recommendation: str
    evidence_fields: List[str]
    guideline_reference: str = None
    confidence: str = "high"


class RuleEngine:
    """
    Expert system rule engine for pain assessment.
    
    Applies deterministic clinical rules to structured pain data and generates
    explainable recommendations with complete evidence chains.
    
    All rules are:
    1. Based on established clinical guidelines
    2. Deterministic (no probabilistic inference)
    3. Fully explainable (evidence is explicitly tracked)
    4. Independently verifiable by medical experts
    """
    
    def __init__(self):
        """Initialize rule engine and load clinical decision rules."""
        self.rules: List[ClinicalRule] = []
        self._initialize_rules()
    
    def _initialize_rules(self):
        """
        Initialize clinical decision rules.
        
        Each rule is based on established clinical guidelines and pain management
        best practices. Rules are evaluated in order of priority.
        """
        
        # ========== RULE A: Chronic Pain + Depressive Symptoms → Behavioral Therapy ==========
        def rule_a_condition(pain_data: PainOntology) -> bool:
            """
            Chronic pain with significant affective distress requires multimodal approach.
            
            Based on: Wisconsin Medical Examining Board Guidelines for Chronic Pain Management
            Rationale: Chronic pain with depression benefits from CBT and non-pharmacologic
                      interventions before considering pharmacological escalation.
            """
            temporal_chronic = (
                pain_data.temporal_pattern and 
                ("chronic" in pain_data.temporal_pattern.lower() or "months" in pain_data.temporal_pattern.lower())
            )
            
            emotion_depressed = pain_data.emotion and any(
                term in pain_data.emotion.lower() 
                for term in ["depressed", "depression", "despair", "hopeless"]
            )
            
            return temporal_chronic and emotion_depressed
        
        self.rules.append(ClinicalRule(
            rule_id="RULE_A",
            name="Chronic Pain + Depression → Behavioral Therapy",
            condition=rule_a_condition,
            recommendation=(
                "Recommend behavioral therapy (CBT) per Wisconsin chronic pain guidelines. "
                "Chronic pain with significant depressive symptoms benefits from "
                "culturally concordant behavioral interventions. Prioritize non-pharmacologic "
                "multidisciplinary care before considering pharmacological escalation."
            ),
            evidence_fields=["temporal_pattern", "emotion"],
            guideline_reference="Wisconsin Medical Examining Board Guidelines for Chronic Pain Management",
            confidence="high"
        ))
        
        # ========== RULE B: Neuropathic Pain in Distal Extremities → Peripheral Neuropathy Screening ==========
        def rule_b_condition(pain_data: PainOntology) -> bool:
            """
            Neuropathic pain in hands/feet suggests peripheral neuropathy.
            
            Rationale: Classic presentation of peripheral neuropathy includes neuropathic
                      pain descriptors (electric-shock, tingling, burning) in distal extremities.
                      Requires screening for underlying causes (diabetes, vitamin B12 deficiency, etc.)
            """
            neuropathic = pain_data.pain_type and "neuropathic" in pain_data.pain_type.lower()
            
            distal_location = pain_data.location and any(
                loc in pain_data.location.lower() 
                for loc in ["lower extremities", "feet", "hands", "legs", "arms", "extremities"]
            )
            
            return neuropathic and distal_location
        
        self.rules.append(ClinicalRule(
            rule_id="RULE_B",
            name="Neuropathic Pain in Distal Extremities → Peripheral Neuropathy Screening",
            condition=rule_b_condition,
            recommendation=(
                "Recommend peripheral neuropathy screening. "
                "Neuropathic pain in distal extremities suggests possible peripheral nerve pathology. "
                "Consider neurological examination and investigation of potential underlying causes "
                "(diabetes mellitus, vitamin B12 deficiency, autoimmune conditions, medication toxicity)."
            ),
            evidence_fields=["pain_type", "location"],
            confidence="high"
        ))
        
        # ========== RULE C: Severe Functional Impact → Multidisciplinary Pain Clinic Referral ==========
        def rule_c_condition(pain_data: PainOntology) -> bool:
            """
            Severe functional impairment requires comprehensive pain management.
            
            Rationale: Pain causing significant functional disability (sleep, work, mobility)
                      often requires multidisciplinary approach beyond primary care.
            """
            has_functional_impact = pain_data.functional_impact is not None
            
            severe_impact = has_functional_impact and any(
                term in pain_data.functional_impact.lower()
                for term in ["severe", "unable", "cannot", "impossible", "interfere", "disability"]
            )
            
            return severe_impact
        
        self.rules.append(ClinicalRule(
            rule_id="RULE_C",
            name="Severe Functional Impact → Multidisciplinary Pain Clinic",
            condition=rule_c_condition,
            recommendation=(
                "Consider referral to multidisciplinary pain clinic. "
                "Severe functional impairment indicates need for comprehensive pain management "
                "involving physical therapy, occupational therapy, psychological support, and "
                "coordinated medical management."
            ),
            evidence_fields=["functional_impact", "temporal_pattern"],
            confidence="high"
        ))
        
        # ========== RULE D: Burning Pain → Consider Inflammatory or Neuropathic Etiology ==========
        def rule_d_condition(pain_data: PainOntology) -> bool:
            """
            Burning pain quality suggests specific etiologies.
            
            Rationale: Burning pain can indicate inflammatory processes or small fiber neuropathy.
            """
            burning_pain = pain_data.pain_type and "burning" in pain_data.pain_type.lower()
            return burning_pain
        
        self.rules.append(ClinicalRule(
            rule_id="RULE_D",
            name="Burning Pain → Inflammatory/Neuropathic Workup",
            condition=rule_d_condition,
            recommendation=(
                "Burning pain quality suggests possible inflammatory or small fiber neuropathic etiology. "
                "Consider evaluation for inflammatory conditions, nerve injury, or small fiber neuropathy. "
                "May benefit from topical treatments or neuropathic pain medications."
            ),
            evidence_fields=["pain_type", "location"],
            confidence="medium"
        ))
    
    def evaluate(self, pain_data: PainOntology) -> List[ClinicalRecommendation]:
        """
        Apply all rules to the pain data and return triggered recommendations.
        
        Each recommendation includes:
        - The clinical recommendation text
        - Which rule triggered it
        - The specific evidence (field values) that triggered the rule
        - Confidence level
        - Guideline reference
        
        Args:
            pain_data: Structured pain ontology data
            
        Returns:
            List of clinical recommendations with complete evidence chains
            
        Example:
            >>> pain = PainOntology(
            ...     pain_type="Neuropathic (Electric-shock-like)",
            ...     location="Lower extremities",
            ...     temporal_pattern="Chronic (4 months)",
            ...     emotion="Depressed"
            ... )
            >>> engine = RuleEngine()
            >>> recommendations = engine.evaluate(pain)
            >>> # Returns recommendations for RULE_A and RULE_B
        """
        recommendations = []
        
        for rule in self.rules:
            try:
                if rule.condition(pain_data):
                    # Extract evidence from specified fields
                    evidence = {}
                    for field in rule.evidence_fields:
                        if hasattr(pain_data, field):
                            value = getattr(pain_data, field)
                            if value is not None:  # Only include non-None values
                                evidence[field] = value
                    
                    recommendations.append(ClinicalRecommendation(
                        recommendation=rule.recommendation,
                        triggered_by_rule=f"{rule.rule_id}: {rule.name}",
                        evidence=evidence,
                        confidence=rule.confidence,
                        guideline_reference=rule.guideline_reference
                    ))
            except Exception as e:
                # Log error but continue evaluating other rules
                print(f"Warning: Error evaluating {rule.rule_id}: {str(e)}")
                continue
        
        return recommendations
    
    def generate_reasoning_chain(
        self, 
        pain_data: PainOntology, 
        recommendations: List[ClinicalRecommendation],
        ontology_mappings: List[Dict]
    ) -> List[str]:
        """
        Generate human-readable reasoning chain showing complete decision pathway.
        
        This provides full transparency from patient input to clinical recommendations,
        enabling clinical validation and building trust in the system.
        
        The reasoning chain includes:
        1. Ontology mapping (Chinese → English medical terms)
        2. Structured pain data extraction
        3. Rule evaluation and triggers
        4. Final recommendations with evidence
        
        Args:
            pain_data: Structured pain ontology data
            recommendations: List of triggered recommendations
            ontology_mappings: List of multilingual→English mappings
            
        Returns:
            List of reasoning step strings
            
        Example output:
            [
                "=== Ontology Mapping ===",
                "Input '电击一样' → Mapped to 'Electric-shock-like (Neuropathic)'",
                "=== Structured Pain Data ===",
                "Pain Type: Neuropathic (Electric-shock-like)",
                "=== Rule Engine Evaluation ===",
                "✓ Triggered: RULE_B",
                "  Evidence: {'pain_type': 'Neuropathic', 'location': 'Lower extremities'}"
            ]
        """
        chain = []
        
        # ===== Step 1: Show ontology mappings =====
        chain.append("=== Ontology Mapping ===")
        if ontology_mappings:
            for mapping in ontology_mappings:
                # Show: matched text in user input → dictionary term → English translation
                matched = mapping.get('matched_text', mapping.get('original_term', 'N/A'))
                original = mapping.get('original_term', 'N/A')
                english = mapping.get('mapped_english', 'N/A')
                
                # Display: what user said → what it matches → English term
                if matched != original:
                    display_input = f"{matched} (matches '{original}')"
                else:
                    display_input = matched
                
                chain.append(
                    f"Input '{display_input}' → "
                    f"Mapped to '{english}' "
                    f"({mapping.get('dimension', 'N/A')}, confidence: {mapping.get('confidence', 'N/A')})"
                )
        else:
            chain.append("No specific pain descriptors were mapped from ontology dictionary.")
        
        # ===== Step 2: Show structured data extraction =====
        chain.append("\n=== Structured Pain Data ===")
        chain.append(f"Pain Type: {pain_data.pain_type}")
        chain.append(f"Location: {pain_data.location}")
        chain.append(f"Temporal Pattern: {pain_data.temporal_pattern}")
        if pain_data.intensity and pain_data.intensity != "Not explicitly stated":
            chain.append(f"Intensity: {pain_data.intensity}")
        if pain_data.emotion:
            chain.append(f"Emotional Dimension: {pain_data.emotion}")
        if pain_data.functional_impact:
            chain.append(f"Functional Impact: {pain_data.functional_impact}")
        
        # ===== Step 3: Show rule triggers and recommendations =====
        chain.append("\n=== Rule Engine Evaluation ===")
        if recommendations:
            for rec in recommendations:
                chain.append(f"✓ Triggered: {rec.triggered_by_rule}")
                chain.append(f"  Evidence: {rec.evidence}")
                chain.append(f"  → Recommendation: {rec.recommendation}")
                if rec.guideline_reference:
                    chain.append(f"  → Guideline: {rec.guideline_reference}")
                chain.append("")  # Blank line for readability
        else:
            chain.append("No specific clinical rules triggered.")
            chain.append("Standard pain assessment and management pathway recommended.")
        
        return chain
    
    def add_rule(self, rule: ClinicalRule):
        """
        Add a custom clinical rule to the engine.
        
        This allows for dynamic rule expansion and customization based on
        specific clinical contexts or institutional guidelines.
        
        Args:
            rule: ClinicalRule instance to add
        """
        self.rules.append(rule)
    
    def get_rule_count(self) -> int:
        """Return the number of active rules in the engine."""
        return len(self.rules)
    
    def get_rule_ids(self) -> List[str]:
        """Return list of all rule IDs for reference."""
        return [rule.rule_id for rule in self.rules]