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 sympyStep 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 interfacesympy: Provides symbolic logic operationssatisfiable: Checks if logical expressions can be satisfiedto_cnf: Converts expressions to Conjunctive Normal Formcombinations: 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
keyfor storage and a human-readabletextSYMPTOM_SYMBOLS: Creates SymPy symbolic variables for each symptomSYMPTOM_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 conditionmin_conditions: Minimum number of symptoms needed to trigger the ruleconfidence: 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, readableExplanation:
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 possibleConfidence 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
FalseReturns:
(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 diagnosesExplanation:
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 trueIf 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"]
)Sidebar Stats¶
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"]] = FalseExplanation:
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¶
Save the code as diagnostic_chatbot.py and run:
streamlit run diagnostic_chatbot.pyThe 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¶
Knowledge Base Built:
kb = { "fever": True, "cough": True, "fatigue": True, "sore_throat": False, "runny_nose": False # ... other symptoms False }Flu Rule Evaluated:
rule = { "name": "Flu", "conditions": ["fever", "body_aches", "cough", "fatigue"], "min_conditions": 3 }FOL Expression Generated:
(fever ∧ body_aches ∧ cough) ∨ (fever ∧ body_aches ∧ fatigue) ∨ (fever ∧ cough ∧ fatigue) ∨ (body_aches ∧ cough ∧ fatigue)Substitution:
# Third disjunct: (True ∧ True ∧ True) = True # Expression evaluates to True!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:¶
Fuzzy Logic: Handle symptom severity (mild, moderate, severe)
Temporal Logic: Consider symptom timeline and progression
Probabilistic Reasoning: Integrate Bayesian networks
Database Integration: Store patient history
Medical Ontologies: Use standard medical terminologies (SNOMED-CT)
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¶
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
)