Spaces:
Sleeping
Sleeping
| """ | |
| Inference Engine — formal logical reasoning without an LLM. | |
| Handles: | |
| · Modus Ponens: If P → Q and P, then Q | |
| · Modus Tollens: If P → Q and ¬Q, then ¬P | |
| · Hypothetical Syllogism: If P → Q and Q → R, then P → R | |
| · Disjunctive Syllogism: If P ∨ Q and ¬P, then Q | |
| · Categorical Syllogisms: All A are B; X is A; therefore X is B | |
| · Propositional evaluation: "P and Q", "P or Q", "not P", "P implies Q" | |
| · Contradiction detection: returns False when premises are contradictory | |
| · Consistency checking: verifies a set of statements is mutually consistent | |
| All processing is pure Python — zero external dependencies. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Data structures | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| class InferenceResult: | |
| """Result from the inference engine.""" | |
| conclusion: str = "" | |
| valid: bool = False | |
| rule_applied: str = "" | |
| proof_steps: list[str] = field(default_factory=list) | |
| confidence: str = "HIGH" # HIGH / MEDIUM / LOW | |
| is_tautology: Optional[bool] = None | |
| truth_value: Optional[bool] = None | |
| def to_response(self) -> str: | |
| lines = [f"Conclusion: {self.conclusion}"] | |
| if self.rule_applied: | |
| lines.append(f"Inference rule: {self.rule_applied}") | |
| if self.proof_steps: | |
| lines.append("Proof:") | |
| for i, step in enumerate(self.proof_steps, 1): | |
| lines.append(f" {i}. {step}") | |
| if self.is_tautology is not None: | |
| lines.append(f"Tautology: {'Yes' if self.is_tautology else 'No'}") | |
| lines.append(f"Argument is: {'VALID' if self.valid else 'INVALID'}") | |
| return "\n".join(lines) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Natural language premise parser | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def _extract_if_then(text: str) -> Optional[tuple[str, str]]: | |
| """Extract (antecedent, consequent) from 'if P then Q' patterns.""" | |
| patterns = [ | |
| r"if\s+(.+?)\s+then\s+(.+?)[\.,;]?$", | |
| r"(.+?)\s+implies\s+(.+?)[\.,;]?$", | |
| r"(.+?)\s+→\s+(.+?)[\.,;]?$", | |
| r"when\s+(.+?)[,]\s+(.+?)[\.,;]?$", | |
| r"whenever\s+(.+?)[,]\s+(.+?)[\.,;]?$", | |
| ] | |
| for pat in patterns: | |
| m = re.search(pat, text.lower().strip()) | |
| if m: | |
| return m.group(1).strip(), m.group(2).strip() | |
| return None | |
| _ALL_VERBS = ( | |
| "are", "have", "is", "require", "need", "must", "contain", | |
| "use", "produce", "involve", "consist of", "depend on", | |
| ) | |
| _ALL_VERB_RE = re.compile( | |
| r"all\s+(.+?)\s+" | |
| r"(are|have|is|require|need|must|contain|use|produce|involve|consist\s+of|depend\s+on)" | |
| r"\s+(.+?)[\.,;]?$" | |
| ) | |
| def _extract_all_are(text: str) -> Optional[tuple[str, str, str]]: | |
| """ | |
| Extract (category, property, predicate_verb) from | |
| 'All A are/have/require/need/must/use B' patterns. | |
| Returns (category, property, verb). | |
| """ | |
| t = text.lower().strip() | |
| m = _ALL_VERB_RE.search(t) | |
| if m: | |
| return m.group(1).strip(), m.group(3).strip(), m.group(2).strip() | |
| return None | |
| def _extract_negation(text: str) -> Optional[str]: | |
| """Extract the negated claim from negation patterns.""" | |
| t = text.lower().strip(" .,;?!") | |
| # Starts-with patterns | |
| for prefix in ["not ", "¬", "it is not the case that ", "it is false that "]: | |
| if t.startswith(prefix): | |
| return t[len(prefix):].strip() | |
| # Mid-sentence negation: "X did not Y" → "X will Y" / "X does Y" / "X is Y" | |
| m = re.match(r"^(.+?)\s+did\s+not\s+(.+)$", t) | |
| if m: | |
| subject, verb_phrase = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} will {verb_phrase}" | |
| # "X does not Y" → "X does Y" | |
| m = re.match(r"^(.+?)\s+does\s+not\s+(.+)$", t) | |
| if m: | |
| subject, verb_phrase = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} {verb_phrase}" | |
| # "X do not Y" → "X Y" (plural subjects) | |
| m = re.match(r"^(.+?)\s+do\s+not\s+(.+)$", t) | |
| if m: | |
| subject, verb_phrase = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} {verb_phrase}" | |
| # "X is not Y" → "X is Y" | |
| m = re.match(r"^(.+?)\s+is\s+not\s+(.+)$", t) | |
| if m: | |
| subject, obj = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} is {obj}" | |
| # "X are not Y" → "X are Y" | |
| m = re.match(r"^(.+?)\s+are\s+not\s+(.+)$", t) | |
| if m: | |
| subject, obj = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} are {obj}" | |
| # "X was not Y" → "X was Y" | |
| m = re.match(r"^(.+?)\s+was\s+not\s+(.+)$", t) | |
| if m: | |
| subject, obj = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} was {obj}" | |
| # "X has not Y-ed" → "X has Y-ed" | |
| m = re.match(r"^(.+?)\s+has\s+not\s+(.+)$", t) | |
| if m: | |
| subject, obj = m.group(1).strip(), m.group(2).strip() | |
| return f"{subject} has {obj}" | |
| return None | |
| def _normalise(text: str) -> str: | |
| return text.lower().strip(" .,;?!") | |
| def _negate_clause(clause: str) -> str: | |
| """ | |
| Produce a natural-language negation of a simple clause. | |
| e.g. 'the temperature drops' → 'the temperature does not drop' | |
| 'it rains' → 'it does not rain' | |
| 'she is happy' → 'she is not happy' | |
| """ | |
| t = clause.lower().strip() | |
| # "X is/are/was/were Y" → "X is/are/was/were not Y" | |
| for verb in ("is", "are", "was", "were"): | |
| pat = rf"^(.*?)\b{verb}\b(.*)$" | |
| m = re.match(pat, t) | |
| if m: | |
| return f"{m.group(1).strip()} {verb} not{m.group(2)}" | |
| # "X will Y" → "X will not Y" | |
| m = re.match(r"^(.*?)\bwill\b(.*)$", t) | |
| if m: | |
| return f"{m.group(1).strip()} will not{m.group(2)}" | |
| # "X can Y" → "X cannot Y" | |
| m = re.match(r"^(.*?)\bcan\b(.*)$", t) | |
| if m: | |
| return f"{m.group(1).strip()} cannot{m.group(2)}" | |
| # General: for simple clauses (≤5 words) treat the last word as the verb | |
| # For longer clauses, use "does not" before the second-to-last meaningful word | |
| words = t.split() | |
| if len(words) == 1: | |
| return f"not {clause}" | |
| if len(words) <= 5: | |
| # Last word is the main verb; everything before it is the subject | |
| subject_part = " ".join(words[:-1]) | |
| verb_part = words[-1] | |
| return f"{subject_part} does not {verb_part}" | |
| # Longer clause: insert 'does not' after first two words | |
| subject_part = " ".join(words[:2]) | |
| verb_part = " ".join(words[2:]) | |
| return f"{subject_part} does not {verb_part}" | |
| def _fuzzy_match(a: str, b: str) -> bool: | |
| """True if a and b are close enough to be considered the same claim.""" | |
| a, b = _normalise(a), _normalise(b) | |
| if a == b: | |
| return True | |
| # Simple stem: strip trailing 's', 'ed', 'ing' | |
| def _stem(w: str) -> str: | |
| for suf in ("ing", "ed", "s"): | |
| if w.endswith(suf) and len(w) > len(suf) + 2: | |
| return w[:-len(suf)] | |
| return w | |
| a_words = {_stem(w) for w in a.split()} | |
| b_words = {_stem(w) for w in b.split()} | |
| if not a_words or not b_words: | |
| return False | |
| overlap = a_words & b_words | |
| union = a_words | b_words | |
| # Jaccard similarity ≥ 0.5 means they are about the same thing | |
| return len(overlap) / len(union) >= 0.5 | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Propositional logic evaluator | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def _eval_prop(expr: str, assignments: dict[str, bool]) -> Optional[bool]: | |
| """ | |
| Evaluate a simple propositional expression given variable assignments. | |
| Supports: AND, OR, NOT, IMPLIES (→ / =>), IFF (<->) | |
| Variables are single uppercase letters or short words. | |
| Returns None if the expression cannot be parsed. | |
| """ | |
| expr = expr.strip() | |
| # Normalise operators | |
| expr = re.sub(r"\bimplies\b", "=>", expr, flags=re.IGNORECASE) | |
| expr = re.sub(r"→", "=>", expr) | |
| expr = re.sub(r"<->|↔", "IFF", expr) | |
| expr = re.sub(r"\band\b", "AND", expr, flags=re.IGNORECASE) | |
| expr = re.sub(r"\bor\b", "OR", expr, flags=re.IGNORECASE) | |
| expr = re.sub(r"\bnot\b|¬", "NOT ", expr, flags=re.IGNORECASE) | |
| # Replace variables with their truth values | |
| for var, val in sorted(assignments.items(), key=lambda x: -len(x[0])): | |
| expr = re.sub(r"\b" + re.escape(var) + r"\b", str(val), expr) | |
| try: | |
| expr_py = ( | |
| expr | |
| .replace("AND", " and ") | |
| .replace("OR", " or ") | |
| .replace("NOT ", " not ") | |
| .replace("=>", " <= ") # P => Q ≡ (not P) or Q | |
| ) | |
| # Handle implication: P => Q is not P or Q | |
| # Rebuild properly: | |
| def _replace_implies(e: str) -> str: | |
| parts = re.split(r"\s*<=\s*", e) | |
| if len(parts) == 2: | |
| return f"(not ({parts[0].strip()}) or ({parts[1].strip()}))" | |
| return e | |
| expr_py2 = _replace_implies(expr_py) | |
| result = eval(expr_py2, {"__builtins__": {}}, {"True": True, "False": False}) | |
| return bool(result) | |
| except Exception: | |
| return None | |
| def _generate_truth_table(variables: list[str], formula: str) -> list[dict]: | |
| """Generate truth table rows for a propositional formula.""" | |
| n = len(variables) | |
| rows = [] | |
| for i in range(2 ** n): | |
| assignment = {} | |
| for j, var in enumerate(variables): | |
| assignment[var] = bool((i >> (n - 1 - j)) & 1) | |
| result = _eval_prop(formula, assignment) | |
| rows.append({**assignment, "result": result}) | |
| return rows | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Inference rules | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def _modus_ponens(premise1: str, premise2: str) -> Optional[InferenceResult]: | |
| """ | |
| Modus Ponens: If P → Q and P, then Q. | |
| premise1 should be the conditional; premise2 should be P. | |
| """ | |
| cond = _extract_if_then(premise1) | |
| if not cond: | |
| cond = _extract_if_then(premise2) | |
| if cond: | |
| premise1, premise2 = premise2, premise1 | |
| if not cond: | |
| return None | |
| antecedent, consequent = cond | |
| p2_norm = _normalise(premise2) | |
| if _fuzzy_match(p2_norm, antecedent) or antecedent in p2_norm: | |
| return InferenceResult( | |
| conclusion=consequent.capitalize(), | |
| valid=True, | |
| rule_applied="Modus Ponens (P → Q, P ⊢ Q)", | |
| proof_steps=[ | |
| f"Premise 1: {premise1.strip()}", | |
| f"Premise 2: {premise2.strip()}", | |
| f"Premise 1 is a conditional: if '{antecedent}' then '{consequent}'", | |
| f"Premise 2 affirms the antecedent: '{antecedent}'", | |
| f"By Modus Ponens, the consequent follows: '{consequent}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| return None | |
| def _modus_tollens(premise1: str, premise2: str) -> Optional[InferenceResult]: | |
| """ | |
| Modus Tollens: If P → Q and ¬Q, then ¬P. | |
| """ | |
| cond = _extract_if_then(premise1) | |
| if not cond: | |
| cond = _extract_if_then(premise2) | |
| if cond: | |
| premise1, premise2 = premise2, premise1 | |
| if not cond: | |
| return None | |
| antecedent, consequent = cond | |
| neg_q = _extract_negation(premise2) | |
| if neg_q and (_fuzzy_match(neg_q, consequent) or consequent in neg_q): | |
| # Build a natural negation of the antecedent | |
| neg_ant = _negate_clause(antecedent) | |
| return InferenceResult( | |
| conclusion=neg_ant.capitalize(), | |
| valid=True, | |
| rule_applied="Modus Tollens (P → Q, ¬Q ⊢ ¬P)", | |
| proof_steps=[ | |
| f"Premise 1: {premise1.strip()}", | |
| f"Premise 2: {premise2.strip()}", | |
| f"Premise 1 is a conditional: if '{antecedent}' then '{consequent}'", | |
| f"Premise 2 denies the consequent: 'not {consequent}'", | |
| f"By Modus Tollens, the antecedent is denied: '{neg_ant}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| return None | |
| def _hypothetical_syllogism(p1: str, p2: str) -> Optional[InferenceResult]: | |
| """ | |
| Hypothetical Syllogism: If P → Q and Q → R, then P → R. | |
| """ | |
| cond1 = _extract_if_then(p1) | |
| cond2 = _extract_if_then(p2) | |
| if not cond1 or not cond2: | |
| return None | |
| ant1, cons1 = cond1 | |
| ant2, cons2 = cond2 | |
| if _normalise(cons1) == _normalise(ant2) or cons1 in ant2: | |
| return InferenceResult( | |
| conclusion=f"If {ant1}, then {cons2}", | |
| valid=True, | |
| rule_applied="Hypothetical Syllogism (P → Q, Q → R ⊢ P → R)", | |
| proof_steps=[ | |
| f"Premise 1: if '{ant1}' then '{cons1}'", | |
| f"Premise 2: if '{ant2}' then '{cons2}'", | |
| f"The consequent of Premise 1 ('{cons1}') matches the antecedent of Premise 2 ('{ant2}')", | |
| f"By Hypothetical Syllogism: if '{ant1}' then '{cons2}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| return None | |
| def _categorical_syllogism(p1: str, p2: str) -> Optional[InferenceResult]: | |
| """ | |
| Categorical Syllogism: All A are/have B; X is A; therefore X is/has B. | |
| """ | |
| all_match = _extract_all_are(p1) | |
| if not all_match: | |
| all_match = _extract_all_are(p2) | |
| if all_match: | |
| p1, p2 = p2, p1 | |
| if not all_match: | |
| return None | |
| category, prop, verb = all_match | |
| p2_norm = _normalise(p2) | |
| is_patterns = [ | |
| rf"\bis\s+a\s+{re.escape(category)}\b", | |
| rf"\bis\s+an\s+{re.escape(category)}\b", | |
| rf"\bis\s+{re.escape(category)}\b", | |
| rf"\bare\s+{re.escape(category)}\b", | |
| rf"\b{re.escape(category)}\b", | |
| ] | |
| def _clean_subject(raw: str) -> str: | |
| """Strip articles and anything after 'is/are/was/were'.""" | |
| # Cut at first verb ('is', 'are', 'was', 'were', 'has') | |
| raw = re.split(r"\s+(?:is|are|was|were|has)\b", raw)[0].strip() | |
| # Remove leading article | |
| raw = re.sub(r"^(?:a|an|the)\s+", "", raw).strip(" .,;?!") | |
| return raw | |
| subject = None | |
| for pat in is_patterns: | |
| m = re.search(pat, p2_norm) | |
| if m: | |
| raw_subj = p2_norm[:m.start()].strip(" .,;?!") | |
| subject = _clean_subject(raw_subj) | |
| if subject: | |
| break | |
| # Fallback: fuzzy match any word in p2 against category | |
| if not subject: | |
| words = p2_norm.split() | |
| for i, word in enumerate(words): | |
| if _fuzzy_match(word, category): | |
| raw = " ".join(words[:i]).strip(" .,;?!") | |
| subject = _clean_subject(raw) or words[0] | |
| break | |
| if subject: | |
| # Build a natural conclusion with correct verb conjugation | |
| if verb == "have": | |
| conclusion = f"{subject.capitalize()} has {prop}" | |
| elif verb in ("are", "is"): | |
| conclusion = f"{subject.capitalize()} is {prop}" | |
| else: | |
| # Preserve original verb (require → requires, need → needs, etc.) | |
| verb_s = verb.rstrip("e") + "s" if not verb.endswith("s") else verb | |
| conclusion = f"{subject.capitalize()} {verb_s} {prop}" | |
| return InferenceResult( | |
| conclusion=conclusion, | |
| valid=True, | |
| rule_applied="Categorical Syllogism (All A are B; X is A ⊢ X is B)", | |
| proof_steps=[ | |
| f"Major premise: All {category} {verb} {prop}", | |
| f"Minor premise: {p2.strip()}", | |
| f"'{subject}' is identified as a member of '{category}'", | |
| f"By categorical syllogism: '{subject}' {verb} '{prop}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| return None | |
| _EMBEDDED_EITHER_RE = re.compile( | |
| r"\beither\s+(.+?)\s+or\s+(.+?)[\.,;]?$" | |
| ) | |
| _GENERAL_OR_RE = re.compile( | |
| r"(?:either\s+)?(.+?)\s+or\s+(.+?)[\.,;]?$" | |
| ) | |
| def _extract_disjuncts(text: str) -> Optional[tuple[str, str]]: | |
| """ | |
| Extract (option_a, option_b) from a disjunction sentence. | |
| Prefers the embedded 'either X or Y' pattern so that surrounding | |
| context ('The prompt is routed to either X or Y') doesn't pollute option_a. | |
| Falls back to the first 'X or Y' found. | |
| """ | |
| t = text.lower().strip() | |
| # Prefer embedded "either X or Y" — strips context before "either" | |
| m = _EMBEDDED_EITHER_RE.search(t) | |
| if m: | |
| return m.group(1).strip(), m.group(2).strip() | |
| # General fallback | |
| m = _GENERAL_OR_RE.search(t) | |
| if m: | |
| return m.group(1).strip(), m.group(2).strip() | |
| return None | |
| def _disjunctive_syllogism(p1: str, p2: str) -> Optional[InferenceResult]: | |
| """ | |
| Disjunctive Syllogism: P ∨ Q and ¬P ⊢ Q. | |
| """ | |
| disjuncts = _extract_disjuncts(p1) | |
| if not disjuncts: | |
| disjuncts = _extract_disjuncts(p2) | |
| if disjuncts: | |
| p1, p2 = p2, p1 | |
| if not disjuncts: | |
| return None | |
| option_a, option_b = disjuncts | |
| neg = _extract_negation(p2) | |
| if neg: | |
| neg_n = _normalise(neg) | |
| if _fuzzy_match(neg, option_a) or option_a in neg_n: | |
| return InferenceResult( | |
| conclusion=option_b.capitalize(), | |
| valid=True, | |
| rule_applied="Disjunctive Syllogism (P ∨ Q, ¬P ⊢ Q)", | |
| proof_steps=[ | |
| f"Disjunction: '{option_a}' OR '{option_b}'", | |
| f"Negation: NOT '{option_a}'", | |
| f"By Disjunctive Syllogism: '{option_b}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| if _fuzzy_match(neg, option_b) or option_b in neg_n: | |
| return InferenceResult( | |
| conclusion=option_a.capitalize(), | |
| valid=True, | |
| rule_applied="Disjunctive Syllogism (P ∨ Q, ¬Q ⊢ P)", | |
| proof_steps=[ | |
| f"Disjunction: '{option_a}' OR '{option_b}'", | |
| f"Negation: NOT '{option_b}'", | |
| f"By Disjunctive Syllogism: '{option_a}'", | |
| ], | |
| confidence="HIGH", | |
| ) | |
| return None | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Public interface | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| class InferenceEngine: | |
| """ | |
| Pure logical inference engine. | |
| Usage: | |
| ie = InferenceEngine() | |
| result = ie.infer(user_input) | |
| if result.valid: | |
| print(result.to_response()) | |
| """ | |
| def infer(self, user_input: str) -> InferenceResult: | |
| """ | |
| Attempt to infer a conclusion from the user's input. | |
| Tries all known inference rules in order. | |
| Returns an InferenceResult with valid=False if nothing can be derived. | |
| """ | |
| # Split input into premises | |
| raw_sentences = re.split(r"[.;]\s+|\n|,\s+and\s+", user_input.strip()) | |
| # Filter out conclusion prompts like "Therefore?", "What follows?", "What can we conclude?" | |
| _CONCLUSION_PROMPTS = re.compile( | |
| r"^(therefore|what follows|what can we conclude|" | |
| r"what\s+is\s+the\s+conclusion|so\s+what|" | |
| r"can\s+we\s+conclude|what\s+do\s+we\s+know)[?.,\s]*$", | |
| re.IGNORECASE, | |
| ) | |
| premises = [ | |
| s.strip() for s in raw_sentences | |
| if len(s.strip()) > 3 and not _CONCLUSION_PROMPTS.match(s.strip()) | |
| ] | |
| if len(premises) < 2: | |
| return InferenceResult( | |
| conclusion="", | |
| valid=False, | |
| rule_applied="", | |
| proof_steps=["At least two premises are required for formal inference"], | |
| ) | |
| # Try pairwise inference | |
| for i in range(len(premises)): | |
| for j in range(len(premises)): | |
| if i == j: | |
| continue | |
| p1, p2 = premises[i], premises[j] | |
| result = _modus_ponens(p1, p2) | |
| if result and result.valid: | |
| return result | |
| result = _modus_tollens(p1, p2) | |
| if result and result.valid: | |
| return result | |
| result = _hypothetical_syllogism(p1, p2) | |
| if result and result.valid: | |
| return result | |
| result = _categorical_syllogism(p1, p2) | |
| if result and result.valid: | |
| return result | |
| result = _disjunctive_syllogism(p1, p2) | |
| if result and result.valid: | |
| return result | |
| return InferenceResult( | |
| conclusion="", | |
| valid=False, | |
| rule_applied="No matching inference rule", | |
| proof_steps=[ | |
| "Premises identified: " + " | ".join(f"'{p}'" for p in premises), | |
| "None of the standard inference rules (Modus Ponens, Modus Tollens, " | |
| "Hypothetical Syllogism, Categorical Syllogism, Disjunctive Syllogism) " | |
| "could be applied to derive a certain conclusion.", | |
| "The argument may require domain knowledge or be invalid.", | |
| ], | |
| ) | |
| def check_consistency(self, statements: list[str]) -> tuple[bool, str]: | |
| """ | |
| Check whether a list of statements is mutually consistent. | |
| Returns (is_consistent, explanation). | |
| """ | |
| if len(statements) < 2: | |
| return True, "Only one statement — trivially consistent." | |
| for i, s1 in enumerate(statements): | |
| for j, s2 in enumerate(statements): | |
| if i >= j: | |
| continue | |
| neg_s1 = _extract_negation(s1) | |
| neg_s2 = _extract_negation(s2) | |
| if neg_s1 and _normalise(neg_s1) == _normalise(s2): | |
| return False, ( | |
| f"Contradiction: '{s1}' directly negates '{s2}'" | |
| ) | |
| if neg_s2 and _normalise(neg_s2) == _normalise(s1): | |
| return False, ( | |
| f"Contradiction: '{s2}' directly negates '{s1}'" | |
| ) | |
| return True, "No direct contradictions detected among the statements." | |
| def evaluate_proposition(self, formula: str) -> InferenceResult: | |
| """ | |
| Evaluate a simple propositional formula with variable assignments. | |
| Example: "P AND Q where P=True, Q=False" | |
| """ | |
| var_m = re.search(r"where\s+(.+)$", formula, re.IGNORECASE) | |
| assignments: dict[str, bool] = {} | |
| if var_m: | |
| formula_part = formula[: var_m.start()].strip() | |
| assign_text = var_m.group(1) | |
| for pair in re.split(r",\s*", assign_text): | |
| pair_m = re.match(r"([A-Za-z]\w*)\s*=\s*(true|false|1|0)", pair.strip(), re.IGNORECASE) | |
| if pair_m: | |
| var_name = pair_m.group(1) | |
| val_str = pair_m.group(2).lower() | |
| assignments[var_name] = val_str in ("true", "1") | |
| else: | |
| formula_part = formula | |
| result = _eval_prop(formula_part, assignments) | |
| if result is None: | |
| return InferenceResult( | |
| conclusion="Cannot evaluate: formula is not in a recognisable form", | |
| valid=False, | |
| ) | |
| vars_in_formula = re.findall(r"\b([A-Z])\b", formula_part) | |
| is_tautology = None | |
| if vars_in_formula and not assignments: | |
| rows = _generate_truth_table(list(dict.fromkeys(vars_in_formula)), formula_part) | |
| if all(r["result"] for r in rows): | |
| is_tautology = True | |
| elif not any(r["result"] for r in rows): | |
| is_tautology = False | |
| return InferenceResult( | |
| conclusion=f"The expression evaluates to: {result}", | |
| valid=True, | |
| rule_applied="Propositional evaluation", | |
| truth_value=result, | |
| is_tautology=is_tautology, | |
| proof_steps=[ | |
| f"Formula: {formula_part}", | |
| f"Assignments: {assignments if assignments else 'none given'}", | |
| f"Result: {result}", | |
| ], | |
| ) | |