Text Generation
PyTorch
English
French
hyperdimensional-computing
spiking-neural-networks
hdc
snn
lif
stdp
r-stdp
brain-inspired
cognitive-architecture
agentic
cpu-only
no-transformer
no-gpu
non-transformer
sparse-distributed-memory
kanerva
attractor-networks
global-workspace-theory
predictive-coding
neuromodulators
consciousness
kuramoto
vector-symbolic-architecture
vsa
one-shot-learning
instant-learning
pure-python
numpy
scipy
fastapi
web-dashboard
multi-modal
bpe
benchmark
beam-search
attention
reinforcement-learning
n-gram
kneser-ney
generative-ai
reasoning
creative-writing
research
prototype
| """ | |
| compositional.py — Compositional reasoning: decompose complex questions. | |
| PROBLEM | |
| ------- | |
| Transformer LLMs answer complex questions in one shot — they often hallucinate | |
| because they can't show their work or backtrack. | |
| SOLUTION | |
| -------- | |
| CompositionalReasoner decomposes a complex question into a tree of | |
| sub-questions, solves each recursively, and combines the answers. | |
| Example: | |
| "What is the capital of the country where the city that hosts the 2024 Olympics is located?" | |
| Decomposition: | |
| Q1: "What city hosts the 2024 Olympics?" → Paris | |
| Q2: "What country is Paris in?" → France | |
| Q3: "What is the capital of France?" → Paris | |
| Tree: | |
| ROOT → Q3(capital_of, Q2(country_of, Q1(host_of, "2024 Olympics"))) | |
| Each sub-question is solved by AETHER's normal pipeline; results are threaded | |
| upward. If any sub-question fails, the reasoner can backtrack and try a | |
| different decomposition. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import List, Tuple, Optional, Dict, Any | |
| from dataclasses import dataclass, field | |
| import logging | |
| log = logging.getLogger(__name__) | |
| class SubQuestion: | |
| """A single sub-question in a compositional decomposition.""" | |
| question: str | |
| subquestions: List["SubQuestion"] = field(default_factory=list) | |
| answer: Optional[str] = None | |
| confidence: float = 0.0 | |
| failed: bool = False | |
| class CompositionalResult: | |
| """Result of compositional reasoning.""" | |
| root: SubQuestion | |
| final_answer: Optional[str] | |
| final_confidence: float | |
| n_subquestions: int | |
| depth: int | |
| decomposition_str: str | |
| class CompositionalReasoner: | |
| """Decompose complex questions into sub-questions.""" | |
| def __init__(self, agent): | |
| self.agent = agent | |
| def answer(self, question: str) -> CompositionalResult: | |
| """Answer a complex question by decomposition.""" | |
| root = self._decompose(question) | |
| self._solve(root) | |
| final = root.answer | |
| conf = root.confidence | |
| decomp = self._format_tree(root) | |
| return CompositionalResult( | |
| root=root, | |
| final_answer=final, | |
| final_confidence=conf, | |
| n_subquestions=self._count_nodes(root) - 1, | |
| depth=self._depth(root), | |
| decomposition_str=decomp, | |
| ) | |
| # ------------------------------------------------------------------ # | |
| # Decomposition patterns | |
| # ------------------------------------------------------------------ # | |
| def _decompose(self, question: str) -> SubQuestion: | |
| """Decompose a question into a tree of sub-questions.""" | |
| q = question.strip().rstrip("?") | |
| lower = q.lower() | |
| # Pattern 1: "What is the capital of the country where X is located?" | |
| m = re.match(r"what is the capital of (?:the )?country where (.+) is located", lower) | |
| if m: | |
| place = m.group(1).strip() | |
| return SubQuestion( | |
| question=question, | |
| subquestions=[ | |
| SubQuestion(f"Where is {place} located?"), # sub_q1 | |
| SubQuestion("What is the capital of $1?"), # placeholder | |
| ], | |
| ) | |
| # Pattern 2: "What is the capital of the country whose capital is X?" | |
| m = re.match(r"what is the capital of (?:the )?country whose capital is (.+)", lower) | |
| if m: | |
| capital = m.group(1).strip() | |
| return SubQuestion( | |
| question=question, | |
| subquestions=[ | |
| SubQuestion(f"What country has {capital} as its capital?"), | |
| SubQuestion("What is the capital of $1?"), | |
| ], | |
| ) | |
| # Pattern 3: "Where is the capital of X located?" | |
| m = re.match(r"where is the capital of (.+) located", lower) | |
| if m: | |
| country = m.group(1).strip() | |
| return SubQuestion( | |
| question=question, | |
| subquestions=[ | |
| SubQuestion(f"What is the capital of {country}?"), | |
| SubQuestion("Where is $1 located?"), | |
| ], | |
| ) | |
| # Pattern 4: "What is the X of the Y of Z?" | |
| m = re.match(r"what is the (\w+) of the (\w+) of (.+)", lower) | |
| if m: | |
| p1, p2, subj = m.groups() | |
| return SubQuestion( | |
| question=question, | |
| subquestions=[ | |
| SubQuestion(f"What is the {p2} of {subj}?"), | |
| SubQuestion(f"What is the {p1} of $1?"), | |
| ], | |
| ) | |
| # Pattern 5: "What is the X of Y?" (single hop, no decomposition) | |
| m = re.match(r"what is the (\w+) of (.+)", lower) | |
| if m: | |
| return SubQuestion(question=question) # leaf | |
| # Fallback: no decomposition | |
| return SubQuestion(question=question) | |
| # ------------------------------------------------------------------ # | |
| # Solve the tree recursively | |
| # ------------------------------------------------------------------ # | |
| def _solve(self, node: SubQuestion) -> None: | |
| """Solve a sub-question node, recursively solving its children first.""" | |
| # First solve all sub-questions | |
| for i, sub in enumerate(node.subquestions): | |
| self._solve(sub) | |
| # Substitute the placeholder $1, $2 with sub-answers | |
| if sub.answer and not sub.failed: | |
| placeholder = f"${i+1}" | |
| for j, later_sub in enumerate(node.subquestions[i+1:], start=i+1): | |
| later_sub.question = later_sub.question.replace(placeholder, sub.answer) | |
| # If there are sub-questions, the answer is the last one's answer | |
| if node.subquestions: | |
| last = node.subquestions[-1] | |
| node.answer = last.answer | |
| node.confidence = last.confidence | |
| node.failed = last.failed | |
| return | |
| # Leaf node: ask AETHER directly | |
| try: | |
| answer = self.agent.ask(node.question) | |
| # Clean up the answer (strip NLG templates) | |
| cleaned = self._extract_answer(answer, node.question) | |
| node.answer = cleaned | |
| node.confidence = 0.9 if cleaned else 0.0 | |
| node.failed = not cleaned | |
| except Exception as e: | |
| log.warning(f"sub-question failed: {node.question!r}: {e}") | |
| node.failed = True | |
| def _extract_answer(self, response: str, question: str) -> Optional[str]: | |
| """Extract the bare answer from a natural-language response.""" | |
| # If the response contains "It's X" or "The X is Y", extract X or Y | |
| # Simple heuristic: take the last capitalized word or the answer after "is" | |
| r = response.strip().rstrip(".") | |
| # Check for known patterns | |
| m = re.search(r"(?:It's|It is)\s+(.+)", r, re.IGNORECASE) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"(?:The capital of \w+ is|capital is)\s+(.+)", r, re.IGNORECASE) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"is located in\s+(.+)", r, re.IGNORECASE) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"is\s+(.+)", r, re.IGNORECASE) | |
| if m: return m.group(1).strip() | |
| # Fallback: return the whole response | |
| return r if len(r) < 50 else None | |
| # ------------------------------------------------------------------ # | |
| # Tree utilities | |
| # ------------------------------------------------------------------ # | |
| def _count_nodes(self, node: SubQuestion) -> int: | |
| return 1 + sum(self._count_nodes(s) for s in node.subquestions) | |
| def _depth(self, node: SubQuestion) -> int: | |
| if not node.subquestions: return 1 | |
| return 1 + max(self._depth(s) for s in node.subquestions) | |
| def _format_tree(self, node: SubQuestion, indent: int = 0) -> str: | |
| prefix = " " * indent | |
| ans = node.answer or "?" | |
| mark = "✓" if not node.failed else "✗" | |
| line = f"{prefix}{mark} Q: {node.question!r}\n{prefix} A: {ans!r}\n" | |
| for sub in node.subquestions: | |
| line += self._format_tree(sub, indent + 1) | |
| return line | |