Building a Medical Diagnostic Chatbot with First Order Logic

In this tutorial, we’ll build an interactive medical diagnostic chatbot that uses First Order Logic (FOL) for inference. This demonstrates how formal logic can be applied to real-world problems like medical diagnosis. We’ll use Python with streamlit for the interface and sympy for logical operations.

What You’ll Learn

  • How to represent medical knowledge as logical rules
  • Forward chaining inference in FOL
  • Building interactive chatbots with Streamlit
  • Using SymPy for logical satisfiability checking
  • Knowledge base consistency validation

Prerequisites

pip install streamlit sympy

Step 1: Project Setup and Imports

Let’s start by importing the necessary libraries and configuring our Streamlit app:

import streamlit as st
from typing import List, Dict, Tuple
import time
from sympy import symbols, And, Or
from sympy.logic import satisfiable
from sympy.logic.boolalg import to_cnf
from itertools import combinations

# Page configuration
st.set_page_config(
    page_title="🏥 FOL Diagnostic Chatbot",
    page_icon="🏥",
    layout="wide",
    initial_sidebar_state="expanded"
)

Explanation:

  • streamlit: Creates our web interface
  • sympy: Provides symbolic logic operations
  • satisfiable: Checks if logical expressions can be satisfied
  • to_cnf: Converts expressions to Conjunctive Normal Form
  • combinations: Generates all possible symptom combinations

Step 2: Initialize Session State

Streamlit uses session state to persist data across reruns:

# Initialize session state
if 'knowledge_base' not in st.session_state:
    st.session_state.knowledge_base = {}
if 'custom_rules' not in st.session_state:
    st.session_state.custom_rules = []

Explanation:

  • knowledge_base: Stores user symptoms as logical facts (e.g., {"fever": True})
  • custom_rules: Allows users to add their own diagnostic rules

Step 3: Define Diagnostic Questions

We create a structured list of symptoms to query:

DIAGNOSTIC_QUESTIONS = [
    {"key": "fever", "text": "Do you have a fever (body temperature above 98.6°F)?"},
    {"key": "cough", "text": "Do you have a cough?"},
    {"key": "sore_throat", "text": "Do you have a sore throat?"},
    {"key": "runny_nose", "text": "Do you have a runny or stuffy nose?"},
    {"key": "body_aches", "text": "Do you experience body aches or muscle pain?"},
    {"key": "fatigue", "text": "Do you feel unusually tired or fatigued?"},
    {"key": "headache", "text": "Do you have a headache?"},
    {"key": "sneezing", "text": "Do you have frequent sneezing?"},
    {"key": "itchy_eyes", "text": "Do you have itchy or watery eyes?"},
    {"key": "sensitivity_to_light", "text": "Are you sensitive to light?"},
    {"key": "nausea", "text": "Do you feel nauseous or have an upset stomach?"},
    {"key": "persistent_cough", "text": "Do you have a persistent cough (lasting more than a week)?"},
    {"key": "chest_discomfort", "text": "Do you feel chest discomfort or chest pain?"},
    {"key": "swollen_tonsils", "text": "Do you have swollen tonsils?"}
]

SYMPTOM_SYMBOLS = {q["key"]: symbols(q["key"]) for q in DIAGNOSTIC_QUESTIONS}
SYMPTOM_NAMES = {q["key"]: q["text"].split("?")[0] for q in DIAGNOSTIC_QUESTIONS}

Explanation:

  • Each question has a unique key for storage and a human-readable text
  • SYMPTOM_SYMBOLS: Creates SymPy symbolic variables for each symptom
  • SYMPTOM_NAMES: Extracts short names for display

Step 4: Define Diagnostic Rules

Rules encode medical knowledge as logical conditions:

BUILT_IN_RULES = [
    {
        "name": "Common Cold",
        "conditions": ["sore_throat", "runny_nose", "cough"],
        "min_conditions": 2,
        "confidence": 0.7,
    },
    {
        "name": "Flu",
        "conditions": ["fever", "body_aches", "cough", "fatigue"],
        "min_conditions": 3,
        "confidence": 0.8,
    },
    {
        "name": "Allergies",
        "conditions": ["runny_nose", "sneezing", "itchy_eyes"],
        "min_conditions": 2,
        "confidence": 0.75,
    },
    {
        "name": "Migraine",
        "conditions": ["headache", "sensitivity_to_light", "nausea"],
        "min_conditions": 2,
        "confidence": 0.8,
    },
    {
        "name": "Bronchitis",
        "conditions": ["persistent_cough", "chest_discomfort", "fatigue"],
        "min_conditions": 2,
        "confidence": 0.75,
    },
    {
        "name": "Strep Throat",
        "conditions": ["sore_throat", "fever", "swollen_tonsils"],
        "min_conditions": 2,
        "confidence": 0.85,
    }
]

Explanation:

  • Each rule represents a potential diagnosis
  • conditions: Symptoms that may indicate this condition
  • min_conditions: Minimum number of symptoms needed to trigger the rule
  • confidence: Base probability of the diagnosis (0.0 to 1.0)

Step 5: Build FOL Expressions

This is the core of our logic system:

def build_fol_expression(rule: Dict) -> Tuple:
    """Build a First Order Logic expression for a diagnostic rule."""
    symptoms = [SYMPTOM_SYMBOLS[cond] for cond in rule["conditions"]]
    min_conditions = rule["min_conditions"]
    
    if len(symptoms) < min_conditions:
        return None, "Invalid rule"

    # Create expression for "at least min_conditions are true"
    combos = list(combinations(symptoms, min_conditions))
    expr = Or(*[And(*combo) for combo in combos])
    
    # Create a human-readable version
    condition_names = [SYMPTOM_NAMES.get(c.name, c.name) for c in symptoms]
    readable_combos = list(combinations(condition_names, min_conditions))
    combo_strs = ["(" + " ∧ ".join(combo) + ")" for combo in readable_combos]
    readable = " ∨ ".join(combo_strs)
    
    return expr, readable

Explanation:

  • Converts rules into FOL expressions
  • For a rule requiring 2 of 3 symptoms [A, B, C], it creates: (A ∧ B) ∨ (A ∧ C) ∨ (B ∧ C)
  • This means “the rule is satisfied if ANY combination of 2 symptoms is present”
  • Returns both a SymPy expression and human-readable text

Example: For “Common Cold” (2 of 3: sore_throat, runny_nose, cough):

(sore_throat ∧ runny_nose) ∨ (sore_throat ∧ cough) ∨ (runny_nose ∧ cough)

Step 6: Evaluate Rules Against Knowledge Base

This function performs the actual inference:

def evaluate_fol_rule(rule: Dict, kb: Dict[str, bool]) -> Tuple[bool, float, List[str]]:
    """Evaluate a rule using FOL, returning satisfaction, confidence, and matched symptoms."""
    expr, _ = build_fol_expression(rule)
    if expr is None:
        return False, 0.0, []

    # Substitute known facts into the expression
    subs_dict = {SYMPTOM_SYMBOLS[s]: v for s, v in kb.items() if s in rule["conditions"]}
    
    if expr.subs(subs_dict) == True:
        matched_symptoms = [s for s, v in kb.items() if v and s in rule["conditions"]]
        confidence = rule["confidence"] * (len(matched_symptoms) / len(rule["conditions"]))
        return True, confidence, matched_symptoms
    
    return False, 0.0, []

Explanation:

  • Takes a rule and the knowledge base (user’s symptoms)
  • Substitutes actual truth values into the FOL expression
  • If the expression evaluates to True, the diagnosis is possible
  • Confidence is scaled by the proportion of matching symptoms

Example: If user has: {fever: True, cough: True, fatigue: False} And rule is “Flu” requiring 3 of 4 symptoms…

  • Only 2/4 match, so expression evaluates to False
  • Returns: (False, 0.0, [])

Step 7: Forward Chaining Inference

Applies all rules to find diagnoses:

def forward_chaining_fol(kb: Dict[str, bool], rules: List[Dict]) -> List[Tuple]:
    """Apply forward chaining using FOL to generate diagnoses."""
    diagnoses = []
    for rule in rules:
        is_satisfied, confidence, matched_symptoms = evaluate_fol_rule(rule, kb)
        if is_satisfied:
            _, readable_expr = build_fol_expression(rule)
            diagnoses.append((
                rule["name"],
                confidence,
                len(matched_symptoms),
                len(rule["conditions"]),
                matched_symptoms,
                readable_expr
            ))
    diagnoses.sort(key=lambda x: x[1], reverse=True)
    return diagnoses

Explanation:

  • Iterates through all diagnostic rules
  • Evaluates each rule against the knowledge base
  • Collects satisfied rules as potential diagnoses
  • Sorts by confidence (highest first)

Step 8: Consistency Checking

Ensures the knowledge base doesn’t contain contradictions:

def check_logical_consistency(kb: Dict[str, bool]) -> Tuple[bool, str]:
    """Check if the knowledge base is logically consistent."""
    if not kb:
        return True, "Knowledge base is empty."
    
    facts = [SYMPTOM_SYMBOLS[s] if v else ~SYMPTOM_SYMBOLS[s] for s, v in kb.items()]
    if not satisfiable(And(*facts)):
        return False, "Knowledge base contains contradictions!"
    return True, "Knowledge base is logically consistent."

Explanation:

  • Converts KB to a conjunction of facts
  • Uses SymPy’s satisfiable() to check if there’s any interpretation that makes all facts true
  • If unsatisfiable, the KB contains logical contradictions (though unlikely with simple boolean symptoms)

Step 9: Build the User Interface

Main Title and Navigation

st.title("🏥 Diagnostic Chatbot with First Order Logic")
st.markdown("An interactive medical assistant using `sympy` for formal logic.")
st.markdown("---")

# Sidebar
st.sidebar.title("Navigation")
page = st.sidebar.radio(
    "Go to:",
    ["🏠 Home", "💬 Diagnostic Interview", "📋 Knowledge Base", 
     "🔍 Diagnosis", "🧮 FOL Expressions", "➕ Add Custom Rule", "🔄 Reset"]
)
st.sidebar.markdown("---")
st.sidebar.markdown("### 📊 Session Stats")
positive_symptoms = sum(1 for v in st.session_state.knowledge_base.values() if v)
st.sidebar.metric("Symptoms Collected", positive_symptoms)
st.sidebar.metric("Diagnoses Available", len(BUILT_IN_RULES) + len(st.session_state.custom_rules))
st.sidebar.metric("Custom Rules", len(st.session_state.custom_rules))

is_consistent, consistency_msg = check_logical_consistency(st.session_state.knowledge_base)
st.sidebar.markdown(f"**KB Status:** {'✅ Consistent' if is_consistent else '❌ Inconsistent'}")

Step 10: Implement Page Views

Diagnostic Interview Page

elif page == "💬 Diagnostic Interview":
    st.header("💬 Diagnostic Interview")
    st.markdown("Please answer the following questions. Your answers will build the logical knowledge base.")
    
    for i, question in enumerate(DIAGNOSTIC_QUESTIONS):
        st.markdown(f"**Question {i+1}:** {question['text']}")
        
        answer = st.radio(
            "Your answer:", ["Select", "Yes", "No"],
            key=f"q_{question['key']}",
            index=["Select", "Yes", "No"].index(
                "Yes" if st.session_state.knowledge_base.get(question["key"]) is True else
                "No" if st.session_state.knowledge_base.get(question["key"]) is False else
                "Select"
            ),
            horizontal=True
        )
        
        if answer == "Yes":
            st.session_state.knowledge_base[question["key"]] = True
        elif answer == "No":
            st.session_state.knowledge_base[question["key"]] = False

Explanation:

  • Presents each question with radio buttons
  • Preserves previous answers using session state
  • Immediately updates the knowledge base

Knowledge Base View

elif page == "📋 Knowledge Base":
    st.header("📋 Knowledge Base")
    st.markdown("These are the logical facts collected from your answers.")
    
    is_consistent, msg = check_logical_consistency(st.session_state.knowledge_base)
    st.info(f"**Consistency Check:** {msg}")

    if not st.session_state.knowledge_base:
        st.info("No facts collected yet. Please complete the interview.")
    else:
        st.subheader("Facts:")
        for symptom, value in st.session_state.knowledge_base.items():
            st.code(f"{symptom} = {value}", language="python")

Diagnosis Results Page

elif page == "🔍 Diagnosis":
    st.header("🔍 Diagnosis Results")
    st.markdown("Diagnoses are inferred by applying FOL rules to the knowledge base.")
    
    if not st.session_state.knowledge_base:
        st.warning("Please complete the interview to get a diagnosis.")
    else:
        all_rules = BUILT_IN_RULES + st.session_state.custom_rules
        diagnoses = forward_chaining_fol(st.session_state.knowledge_base, all_rules)
        
        if not diagnoses:
            st.info("No diagnosis could be inferred from the current symptoms.")
        else:
            for name, confidence, matched, total, matched_symptoms, fol_expr in diagnoses:
                with st.expander(f"**{name}** - {confidence*100:.1f}% confidence", expanded=True):
                    st.progress(confidence)
                    st.markdown(f"**Matched Symptoms:** {matched}/{total}")
                    st.markdown(f"**FOL Rule:** `{fol_expr}`")
                    st.markdown("**Satisfied by:**")
                    for symptom in matched_symptoms:
                        st.markdown(f"- `{symptom} = True`")

Explanation:

  • Runs forward chaining on the knowledge base
  • Displays each diagnosis with confidence score
  • Shows which symptoms matched and the FOL rule used

FOL Expressions View

elif page == "🧮 FOL Expressions":
    st.header("🧮 First Order Logic Expressions")
    st.markdown("These are the formal logic rules used for diagnosis.")
    
    all_rules = BUILT_IN_RULES + st.session_state.custom_rules
    for rule in all_rules:
        st.subheader(rule["name"])
        expr, readable = build_fol_expression(rule)
        st.markdown(f"**Requires at least {rule['min_conditions']} of {len(rule['conditions'])} symptoms.**")
        st.markdown("**SymPy Expression (CNF):**")
        st.code(str(to_cnf(expr)), language="python")
        st.markdown("**Human-Readable:**")
        st.code(readable, language="text")

Custom Rule Creation

elif page == "➕ Add Custom Rule":
    st.header("➕ Add Custom Diagnostic Rule")
    with st.form("custom_rule_form"):
        rule_name = st.text_input("Rule Name *")
        
        selected_symptoms = st.multiselect(
            "Select Symptoms *",
            options=list(SYMPTOM_NAMES.keys()),
            format_func=lambda x: SYMPTOM_NAMES[x]
        )
        
        min_conditions = st.number_input(
            "Minimum Required Symptoms *",
            min_value=1,
            max_value=len(selected_symptoms) if selected_symptoms else 1,
            value=1
        )
        
        confidence = st.slider("Base Confidence *", 0.0, 1.0, 0.75)
        
        if st.form_submit_button("Add Rule"):
            if rule_name and selected_symptoms and min_conditions:
                new_rule = {
                    "name": rule_name,
                    "conditions": selected_symptoms,
                    "min_conditions": min_conditions,
                    "confidence": confidence
                }
                st.session_state.custom_rules.append(new_rule)
                st.success(f"Rule '{rule_name}' added!")
            else:
                st.error("Please fill all required fields.")

Explanation:

  • Allows users to define new diagnostic rules
  • Uses Streamlit forms for better UX
  • Validates inputs before adding to custom rules

Running the Application

📥 Download diagnostic_chatbot.py

Save the code as diagnostic_chatbot.py and run:

streamlit run diagnostic_chatbot.py

The app will open in your browser at http://localhost:8501.

How the Logic Works: A Complete Example

Let’s trace through a complete diagnostic scenario:

Scenario: User has fever, cough, and fatigue

  1. Knowledge Base Built:
    kb = {
        "fever": True,
        "cough": True,
        "fatigue": True,
        "sore_throat": False,
        "runny_nose": False
        # ... other symptoms False
    }
    
  2. Flu Rule Evaluated:
    rule = {
        "name": "Flu",
        "conditions": ["fever", "body_aches", "cough", "fatigue"],
        "min_conditions": 3
    }
    
  3. FOL Expression Generated:
    (fever ∧ body_aches ∧ cough) ∨ 
    (fever ∧ body_aches ∧ fatigue) ∨ 
    (fever ∧ cough ∧ fatigue) ∨ 
    (body_aches ∧ cough ∧ fatigue)
    
  4. Substitution:
    # Third disjunct: (True ∧ True ∧ True) = True
    # Expression evaluates to True!
    
  5. Result:
    • Diagnosis: Flu
    • Confidence: 0.8 * (3/4) = 60%
    • Matched symptoms: [fever, cough, fatigue]

Key Concepts Demonstrated

1. Knowledge Representation

Symptoms are represented as propositional variables in FOL, allowing precise logical reasoning.

2. Inference Mechanism

Forward chaining systematically applies rules to derive conclusions from facts.

3. Logical Satisfiability

Using SymPy’s SAT solver to check consistency and evaluate complex expressions.

4. Uncertainty Handling

Confidence scores provide a measure of diagnostic certainty.

5. Extensibility

The system allows dynamic rule addition without modifying core logic.

Limitations and Future Enhancements

Current Limitations:

  • Binary symptom representation (True/False) - no severity levels
  • Simple confidence calculation
  • No temporal reasoning (symptom duration)
  • No differential diagnosis ranking beyond confidence

Potential Enhancements:

  1. Fuzzy Logic: Handle symptom severity (mild, moderate, severe)
  2. Temporal Logic: Consider symptom timeline and progression
  3. Probabilistic Reasoning: Integrate Bayesian networks
  4. Database Integration: Store patient history
  5. Medical Ontologies: Use standard medical terminologies (SNOMED-CT)
  6. Explanation Generation: Provide detailed reasoning paths

Conclusion

This tutorial demonstrated how First Order Logic can power a practical diagnostic system. By encoding medical knowledge as logical rules and using formal inference methods, we created an explainable AI system where every diagnosis can be traced back to its logical foundation.

The combination of SymPy’s symbolic logic capabilities and Streamlit’s interactive interface makes it easy to build and understand knowledge-based systems.

Complete Source Code

📥 Download diagnostic_chatbot.py
import streamlit as st
from typing import List, Dict, Tuple
import time
from sympy import symbols, And, Or
from sympy.logic import satisfiable
from sympy.logic.boolalg import to_cnf
from itertools import combinations

# Page configuration
st.set_page_config(
    page_title="🏥 FOL Diagnostic Chatbot",
    page_icon="🏥",
    layout="wide",
    initial_sidebar_state="expanded"
)

# --- Model ---

# Initialize session state
if 'knowledge_base' not in st.session_state:
    st.session_state.knowledge_base = {}
if 'custom_rules' not in st.session_state:
    st.session_state.custom_rules = []

# Define symptom symbols for FOL
DIAGNOSTIC_QUESTIONS = [
    {"key": "fever", "text": "Do you have a fever (body temperature above 98.6°F)?"},
    {"key": "cough", "text": "Do you have a cough?"},
    {"key": "sore_throat", "text": "Do you have a sore throat?"},
    {"key": "runny_nose", "text": "Do you have a runny or stuffy nose?"},
    {"key": "body_aches", "text": "Do you experience body aches or muscle pain?"},
    {"key": "fatigue", "text": "Do you feel unusually tired or fatigued?"},
    {"key": "headache", "text": "Do you have a headache?"},
    {"key": "sneezing", "text": "Do you have frequent sneezing?"},
    {"key": "itchy_eyes", "text": "Do you have itchy or watery eyes?"},
    {"key": "sensitivity_to_light", "text": "Are you sensitive to light?"},
    {"key": "nausea", "text": "Do you feel nauseous or have an upset stomach?"},
    {"key": "persistent_cough", "text": "Do you have a persistent cough (lasting more than a week)?"},
    {"key": "chest_discomfort", "text": "Do you feel chest discomfort or chest pain?"},
    {"key": "swollen_tonsils", "text": "Do you have swollen tonsils?"}
]

SYMPTOM_SYMBOLS = {q["key"]: symbols(q["key"]) for q in DIAGNOSTIC_QUESTIONS}

# Symptom name mapping
SYMPTOM_NAMES = {q["key"]: q["text"].split("?")[0] for q in DIAGNOSTIC_QUESTIONS}

# Built-in diagnostic rules using FOL
BUILT_IN_RULES = [
    {
        "name": "Common Cold",
        "conditions": ["sore_throat", "runny_nose", "cough"],
        "min_conditions": 2,
        "confidence": 0.7,
    },
    {
        "name": "Flu",
        "conditions": ["fever", "body_aches", "cough", "fatigue"],
        "min_conditions": 3,
        "confidence": 0.8,
    },
    {
        "name": "Allergies",
        "conditions": ["runny_nose", "sneezing", "itchy_eyes"],
        "min_conditions": 2,
        "confidence": 0.75,
    },
    {
        "name": "Migraine",
        "conditions": ["headache", "sensitivity_to_light", "nausea"],
        "min_conditions": 2,
        "confidence": 0.8,
    },
    {
        "name": "Bronchitis",
        "conditions": ["persistent_cough", "chest_discomfort", "fatigue"],
        "min_conditions": 2,
        "confidence": 0.75,
    },
    {
        "name": "Strep Throat",
        "conditions": ["sore_throat", "fever", "swollen_tonsils"],
        "min_conditions": 2,
        "confidence": 0.85,
    }
]

def build_fol_expression(rule: Dict) -> Tuple:
    """Build a First Order Logic expression for a diagnostic rule."""
    symptoms = [SYMPTOM_SYMBOLS[cond] for cond in rule["conditions"]]
    min_conditions = rule["min_conditions"]
    
    if len(symptoms) < min_conditions:
        return None, "Invalid rule"

    # Create expression for "at least min_conditions are true"
    combos = list(combinations(symptoms, min_conditions))
    expr = Or(*[And(*combo) for combo in combos])
    
    # Create a human-readable version
    condition_names = [SYMPTOM_NAMES.get(c.name, c.name) for c in symptoms]
    readable_combos = list(combinations(condition_names, min_conditions))
    combo_strs = ["(" + " ∧ ".join(combo) + ")" for combo in readable_combos]
    readable = " ∨ ".join(combo_strs)
    
    return expr, readable

def evaluate_fol_rule(rule: Dict, kb: Dict[str, bool]) -> Tuple[bool, float, List[str]]:
    """Evaluate a rule using FOL, returning satisfaction, confidence, and matched symptoms."""
    expr, _ = build_fol_expression(rule)
    if expr is None:
        return False, 0.0, []

    # Substitute known facts into the expression
    subs_dict = {SYMPTOM_SYMBOLS[s]: v for s, v in kb.items() if s in rule["conditions"]}
    
    if expr.subs(subs_dict) == True:
        matched_symptoms = [s for s, v in kb.items() if v and s in rule["conditions"]]
        confidence = rule["confidence"] * (len(matched_symptoms) / len(rule["conditions"]))
        return True, confidence, matched_symptoms
    
    return False, 0.0, []

def forward_chaining_fol(kb: Dict[str, bool], rules: List[Dict]) -> List[Tuple]:
    """Apply forward chaining using FOL to generate diagnoses."""
    diagnoses = []
    for rule in rules:
        is_satisfied, confidence, matched_symptoms = evaluate_fol_rule(rule, kb)
        if is_satisfied:
            _, readable_expr = build_fol_expression(rule)
            diagnoses.append((
                rule["name"],
                confidence,
                len(matched_symptoms),
                len(rule["conditions"]),
                matched_symptoms,
                readable_expr
            ))
    diagnoses.sort(key=lambda x: x[1], reverse=True)
    return diagnoses

def check_logical_consistency(kb: Dict[str, bool]) -> Tuple[bool, str]:
    """Check if the knowledge base is logically consistent."""
    if not kb:
        return True, "Knowledge base is empty."
    
    facts = [SYMPTOM_SYMBOLS[s] if v else ~SYMPTOM_SYMBOLS[s] for s, v in kb.items()]
    if not satisfiable(And(*facts)):
        return False, "Knowledge base contains contradictions!"
    return True, "Knowledge base is logically consistent."

# --- View ---

st.title("🏥 Diagnostic Chatbot with First Order Logic")
st.markdown("An interactive medical assistant using `sympy` for formal logic.")
st.markdown("---")

# Sidebar
st.sidebar.title("Navigation")
page = st.sidebar.radio(
    "Go to:",
    ["🏠 Home", "💬 Diagnostic Interview", "📋 Knowledge Base", 
     "🔍 Diagnosis", "🧮 FOL Expressions", "➕ Add Custom Rule", "🔄 Reset"]
)

st.sidebar.markdown("---")
st.sidebar.markdown("### 📊 Session Stats")
positive_symptoms = sum(1 for v in st.session_state.knowledge_base.values() if v)
st.sidebar.metric("Symptoms Collected", positive_symptoms)
st.sidebar.metric("Diagnoses Available", len(BUILT_IN_RULES) + len(st.session_state.custom_rules))
st.sidebar.metric("Custom Rules", len(st.session_state.custom_rules))

is_consistent, consistency_msg = check_logical_consistency(st.session_state.knowledge_base)
st.sidebar.markdown(f"**KB Status:** {'✅ Consistent' if is_consistent else '❌ Inconsistent'}")

# Page routing
if page == "🏠 Home":
    st.header("Welcome to the FOL-Powered Diagnostic Chatbot! 👋")
    st.markdown("""
    This application demonstrates how **First Order Logic (FOL)** can be used to build a simple diagnostic system. 
    It uses the `sympy` library to perform logical inference.
    
    **How it works:**
    1.  You answer questions about your symptoms.
    2.  Each answer becomes a logical fact in a **Knowledge Base** (e.g., `fever = True`).
    3.  Diagnostic rules, written as **FOL expressions**, are evaluated against the knowledge base.
    4.  If a rule's conditions are met, a potential diagnosis is suggested.
    
    Go to the **Diagnostic Interview** to get started!
    """)
    st.warning("This tool is for educational purposes only and is not a substitute for professional medical advice.")

elif page == "💬 Diagnostic Interview":
    st.header("💬 Diagnostic Interview")
    st.markdown("Please answer the following questions. Your answers will build the logical knowledge base.")
    
    for i, question in enumerate(DIAGNOSTIC_QUESTIONS):
        st.markdown(f"**Question {i+1}:** {question['text']}")
        
        answer = st.radio(
            "Your answer:", ["Select", "Yes", "No"],
            key=f"q_{question['key']}",
            index=["Select", "Yes", "No"].index(
                "Yes" if st.session_state.knowledge_base.get(question["key"]) is True else
                "No" if st.session_state.knowledge_base.get(question["key"]) is False else
                "Select"
            ),
            horizontal=True
        )
        
        if answer == "Yes":
            st.session_state.knowledge_base[question["key"]] = True
        elif answer == "No":
            st.session_state.knowledge_base[question["key"]] = False

elif page == "📋 Knowledge Base":
    st.header("📋 Knowledge Base")
    st.markdown("These are the logical facts collected from your answers.")
    
    is_consistent, msg = check_logical_consistency(st.session_state.knowledge_base)
    st.info(f"**Consistency Check:** {msg}")

    if not st.session_state.knowledge_base:
        st.info("No facts collected yet. Please complete the interview.")
    else:
        st.subheader("Facts:")
        for symptom, value in st.session_state.knowledge_base.items():
            st.code(f"{symptom} = {value}", language="python")

elif page == "🔍 Diagnosis":
    st.header("🔍 Diagnosis Results")
    st.markdown("Diagnoses are inferred by applying FOL rules to the knowledge base.")
    
    if not st.session_state.knowledge_base:
        st.warning("Please complete the interview to get a diagnosis.")
    else:
        all_rules = BUILT_IN_RULES + st.session_state.custom_rules
        diagnoses = forward_chaining_fol(st.session_state.knowledge_base, all_rules)
        
        if not diagnoses:
            st.info("No diagnosis could be inferred from the current symptoms.")
        else:
            for name, confidence, matched, total, matched_symptoms, fol_expr in diagnoses:
                with st.expander(f"**{name}** - {confidence*100:.1f}% confidence", expanded=True):
                    st.progress(confidence)
                    st.markdown(f"**Matched Symptoms:** {matched}/{total}")
                    st.markdown(f"**FOL Rule:** `{fol_expr}`")
                    st.markdown("**Satisfied by:**")
                    for symptom in matched_symptoms:
                        st.markdown(f"- `{symptom} = True`")

elif page == "🧮 FOL Expressions":
    st.header("🧮 First Order Logic Expressions")
    st.markdown("These are the formal logic rules used for diagnosis.")
    
    all_rules = BUILT_IN_RULES + st.session_state.custom_rules
    for rule in all_rules:
        st.subheader(rule["name"])
        expr, readable = build_fol_expression(rule)
        st.markdown(f"**Requires at least {rule['min_conditions']} of {len(rule['conditions'])} symptoms.**")
        st.markdown("**SymPy Expression (CNF):**")
        st.code(str(to_cnf(expr)), language="python")
        st.markdown("**Human-Readable:**")
        st.code(readable, language="text")

elif page == "➕ Add Custom Rule":
    st.header("➕ Add Custom Diagnostic Rule")
    with st.form("custom_rule_form"):
        rule_name = st.text_input("Rule Name *")
        
        selected_symptoms = st.multiselect(
            "Select Symptoms *",
            options=list(SYMPTOM_NAMES.keys()),
            format_func=lambda x: SYMPTOM_NAMES[x]
        )
        
        min_conditions = st.number_input(
            "Minimum Required Symptoms *",
            min_value=1,
            max_value=len(selected_symptoms) if selected_symptoms else 1,
            value=1
        )
        
        confidence = st.slider("Base Confidence *", 0.0, 1.0, 0.75)
        
        if st.form_submit_button("Add Rule"):
            if rule_name and selected_symptoms and min_conditions:
                new_rule = {
                    "name": rule_name,
                    "conditions": selected_symptoms,
                    "min_conditions": min_conditions,
                    "confidence": confidence
                }
                st.session_state.custom_rules.append(new_rule)
                st.success(f"Rule '{rule_name}' added!")
            else:
                st.error("Please fill all required fields.")

elif page == "🔄 Reset":
    st.header("🔄 Reset Session")
    st.warning("This will clear the knowledge base and reset the interview.")
    if st.button("Confirm Reset", type="primary"):
        st.session_state.knowledge_base = {}
        st.success("Session has been reset.")
        time.sleep(1)
        st.rerun()

# Footer
st.markdown("---")
st.markdown(
    "<div style='text-align: center; color: #626C71; font-size: 12px;'>"
    "FOL Diagnostic Chatbot v2.0 | Educational Demo"
    "</div>",
    unsafe_allow_html=True
)

Further Reading

2025

Generating subsets

3 minute read

In this post, we will discuss the problem of generating all subsets of a given set of elements. A subset is a collection of elements that are selected from a...

Exponentiation

2 minute read

Computing the exponentiation of a number is a classic problem in computer science. The problem is to find the value of a number raised to the power of anothe...

The maximum subsequence sum algorithm

2 minute read

The maximum subsequence sum algorithm is a classic problem in computer science. The problem is to find the maximum sum of a contiguous subsequence in an arra...

Back to top ↑

2024

Docker

4 minute read

In this post, I will explain a very indispensable tool for developers. Docker is a tool that allows developers to build, deploy, and run applications using c...

Generating permutations

2 minute read

In this post, we will discuss the problem of generating all permutations of a given set of elements. A permutation is an arrangement of elements in a specifi...

The Convex Hull Problem

3 minute read

The convex hull problem is a problem in computational geometry. It is about finding the smallest convex polygon that contains a given set of points. The conv...

Back to top ↑

2023

CS3401 Projects 1445 1st semester

1 minute read

Project 1: Project management system The aim of this project is to implement a program (java or python) that manages a list of tasks in a project. The pr...

Back to top ↑

2022

Back to top ↑