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_v2.py — Recursive compositional reasoning with persistent traces. | |
| IMPROVEMENTS OVER v1 | |
| -------------------- | |
| v1 was limited to a fixed set of decomposition patterns. | |
| v2 implements: | |
| 1. RECURSIVE DECOMPOSITION | |
| - Any question can be decomposed into sub-questions | |
| - Sub-questions can themselves be decomposed (arbitrary depth) | |
| - Backtracking: if a sub-question fails, try a different decomposition | |
| 2. PERSISTENT REASONING TRACES | |
| - Every step of reasoning is stored as an HD vector | |
| - Traces can be retrieved and reused for similar future questions | |
| - "I solved something like this before" — true analogical transfer | |
| 3. SYNTHESIS | |
| - Sub-answers are bundled (not just threaded) into a synthesis | |
| - The synthesis HD vector captures the gist of the reasoning | |
| - Can be queried: "what was the gist of my reasoning?" | |
| 4. PLAN REUSE | |
| - When a similar question is asked later, the previous plan is retrieved | |
| - Avoids re-decomposing the same kind of question | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import time | |
| from typing import List, Tuple, Optional, Dict, Any | |
| from dataclasses import dataclass, field | |
| import logging | |
| from .hd import HDVector, DIM, bundle | |
| log = logging.getLogger(__name__) | |
| class ReasoningTrace: | |
| """A persistent trace of a reasoning episode.""" | |
| question: str | |
| question_vec: Any # HDVector | |
| sub_questions: List["ReasoningTrace"] = field(default_factory=list) | |
| answer: Optional[str] = None | |
| confidence: float = 0.0 | |
| failed: bool = False | |
| decomposition_strategy: str = "" | |
| timestamp: float = field(default_factory_factory=time.time) if False else 0.0 | |
| def __post_init__(self): | |
| if self.timestamp == 0.0: | |
| self.timestamp = time.time() | |
| class CompositionalResultV2: | |
| """Result of v2 compositional reasoning.""" | |
| root: ReasoningTrace | |
| final_answer: Optional[str] | |
| final_confidence: float | |
| n_subquestions: int | |
| depth: int | |
| decomposition_str: str | |
| synthesis_vector: Optional[Any] = None # HDVector | |
| reused_trace: bool = False # was a previous trace reused? | |
| class CompositionalReasonerV2: | |
| """Recursive compositional reasoning with persistent traces.""" | |
| # Maximum decomposition depth (prevents infinite recursion) | |
| MAX_DEPTH = 5 | |
| # Minimum confidence to accept an answer | |
| MIN_CONFIDENCE = 0.3 | |
| def __init__(self, agent): | |
| self.agent = agent | |
| # Persistent trace store (HD-vector-addressed) | |
| self.traces: List[ReasoningTrace] = [] | |
| # Synthesis vectors for retrieval | |
| self.synthesis_store: List[Tuple[HDVector, ReasoningTrace]] = [] | |
| def answer(self, question: str) -> CompositionalResultV2: | |
| """Answer a complex question by recursive decomposition.""" | |
| # First: try to find a similar previous trace | |
| q_vec = self.agent.encoder.encode_text(question) | |
| reused = self._find_similar_trace(q_vec) | |
| if reused and reused.answer: | |
| log.info("reusing previous reasoning trace") | |
| return CompositionalResultV2( | |
| root=reused, final_answer=reused.answer, | |
| final_confidence=reused.confidence, | |
| n_subquestions=self._count_nodes(reused) - 1, | |
| depth=self._depth(reused), | |
| decomposition_str=self._format_tree(reused), | |
| synthesis_vector=self._compute_synthesis(reused), | |
| reused_trace=True, | |
| ) | |
| # Decompose recursively | |
| root = self._decompose_recursive(question, depth=0) | |
| self._solve_recursive(root) | |
| # Build synthesis vector | |
| synthesis = self._compute_synthesis(root) | |
| # Store the trace | |
| self.traces.append(root) | |
| self.synthesis_store.append((synthesis, root)) | |
| if len(self.traces) > 100: | |
| self.traces = self.traces[-100:] | |
| self.synthesis_store = self.synthesis_store[-100:] | |
| return CompositionalResultV2( | |
| root=root, final_answer=root.answer, | |
| final_confidence=root.confidence, | |
| n_subquestions=self._count_nodes(root) - 1, | |
| depth=self._depth(root), | |
| decomposition_str=self._format_tree(root), | |
| synthesis_vector=synthesis, | |
| reused_trace=False, | |
| ) | |
| # ------------------------------------------------------------------ # | |
| # Recursive decomposition | |
| # ------------------------------------------------------------------ # | |
| def _decompose_recursive(self, question: str, depth: int) -> ReasoningTrace: | |
| """Decompose a question into sub-questions, recursively.""" | |
| trace = ReasoningTrace( | |
| question=question, | |
| question_vec=self.agent.encoder.encode_text(question), | |
| timestamp=time.time(), | |
| ) | |
| if depth >= self.MAX_DEPTH: | |
| trace.decomposition_strategy = "max_depth_reached" | |
| return trace | |
| # Try each decomposition strategy | |
| for strategy_name, strategy_fn in [ | |
| ("capital_of_country_where", self._decomp_capital_of_country_where), | |
| ("where_is_capital_of", self._decomp_where_is_capital_of), | |
| ("what_is_x_of_y_of_z", self._decomp_what_is_x_of_y_of_z), | |
| ("comparison", self._decomp_comparison), | |
| ("nested_question", self._decomp_nested_question), | |
| ]: | |
| sub_qs = strategy_fn(question) | |
| if sub_qs: | |
| trace.decomposition_strategy = strategy_name | |
| trace.sub_questions = [ | |
| self._decompose_recursive(sq, depth + 1) for sq in sub_qs | |
| ] | |
| return trace | |
| # No decomposition: leaf node | |
| trace.decomposition_strategy = "leaf" | |
| return trace | |
| # ------------------------------------------------------------------ # | |
| # Decomposition strategies | |
| # ------------------------------------------------------------------ # | |
| def _decomp_capital_of_country_where(self, q: str) -> Optional[List[str]]: | |
| m = re.match(r"what is the capital of (?:the )?country where (.+) is located", q, re.I) | |
| if m: | |
| place = m.group(1).strip() | |
| return [f"Where is {place} located?", "What is the capital of $1?"] | |
| return None | |
| def _decomp_where_is_capital_of(self, q: str) -> Optional[List[str]]: | |
| m = re.match(r"where is the capital of (.+) located", q, re.I) | |
| if m: | |
| country = m.group(1).strip() | |
| return [f"What is the capital of {country}?", "Where is $1 located?"] | |
| return None | |
| def _decomp_what_is_x_of_y_of_z(self, q: str) -> Optional[List[str]]: | |
| m = re.match(r"what is the (\w+) of the (\w+) of (.+)", q, re.I) | |
| if m: | |
| p1, p2, subj = m.groups() | |
| return [f"What is the {p2} of {subj}?", f"What is the {p1} of $1?"] | |
| return None | |
| def _decomp_comparison(self, q: str) -> Optional[List[str]]: | |
| m = re.match(r"compare (.+) and (.+)", q, re.I) | |
| if m: | |
| a, b = m.groups() | |
| return [f"What is {a}?", f"What is {b}?", "Compare $1 and $2."] | |
| return None | |
| def _decomp_nested_question(self, q: str) -> Optional[List[str]]: | |
| # Generic: "What is the X of Y?" where Y is itself complex | |
| m = re.match(r"what is the (\w+) of (.+)", q, re.I) | |
| if m and len(m.group(2).split()) > 2: | |
| pred, subj = m.groups() | |
| # Try to decompose subj further | |
| # e.g., "What is the capital of the country where Lyon is located?" | |
| sub = self._decomp_capital_of_country_where(f"What is the {pred} of {subj}?") | |
| if sub: return sub | |
| return None | |
| # ------------------------------------------------------------------ # | |
| # Recursive solving | |
| # ------------------------------------------------------------------ # | |
| def _solve_recursive(self, trace: ReasoningTrace) -> None: | |
| """Solve a trace node, recursively solving its children first.""" | |
| # Solve all sub-questions first | |
| for i, sub in enumerate(trace.sub_questions): | |
| self._solve_recursive(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(trace.sub_questions[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 trace.sub_questions: | |
| last = trace.sub_questions[-1] | |
| trace.answer = last.answer | |
| trace.confidence = last.confidence | |
| trace.failed = last.failed | |
| return | |
| # Leaf node: ask AETHER directly | |
| try: | |
| answer = self.agent.ask(trace.question) | |
| cleaned = self._extract_answer(answer, trace.question) | |
| trace.answer = cleaned | |
| trace.confidence = 0.9 if cleaned else 0.0 | |
| trace.failed = not cleaned or trace.confidence < self.MIN_CONFIDENCE | |
| except Exception as e: | |
| log.warning(f"leaf failed: {trace.question!r}: {e}") | |
| trace.failed = True | |
| def _extract_answer(self, response: str, question: str) -> Optional[str]: | |
| """Extract the bare answer from a natural-language response.""" | |
| r = response.strip().rstrip(".") | |
| m = re.search(r"(?:It's|It is)\s+(.+)", r, re.I) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"(?:The capital of \w+ is|capital is|capital of \w+ is)\s+(.+)", r, re.I) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"is located in\s+(.+)", r, re.I) | |
| if m: return m.group(1).strip() | |
| m = re.search(r"is\s+(.+)", r, re.I) | |
| if m: return m.group(1).strip() | |
| return r if len(r) < 50 else None | |
| # ------------------------------------------------------------------ # | |
| # Synthesis | |
| # ------------------------------------------------------------------ # | |
| def _compute_synthesis(self, trace: ReasoningTrace) -> HDVector: | |
| """Compute the synthesis HD vector of a reasoning trace. | |
| The synthesis bundles the question vector with all sub-answers' | |
| vectors. It captures the gist of the reasoning. | |
| """ | |
| vecs = [trace.question_vec] | |
| for sub in trace.sub_questions: | |
| if sub.answer: | |
| vecs.append(self.agent.encoder.encode_text(sub.answer)) | |
| return bundle(vecs) | |
| # ------------------------------------------------------------------ # | |
| # Trace retrieval (analogical transfer) | |
| # ------------------------------------------------------------------ # | |
| def _find_similar_trace(self, q_vec: HDVector, threshold: float = 0.7) -> Optional[ReasoningTrace]: | |
| """Find a similar previous trace for analogical transfer.""" | |
| if not self.synthesis_store: return None | |
| best_trace = None | |
| best_sim = -1.0 | |
| for syn_vec, trace in self.synthesis_store: | |
| sim = q_vec.similarity(syn_vec) | |
| if sim > best_sim: | |
| best_sim = sim | |
| best_trace = trace | |
| if best_sim >= threshold: | |
| return best_trace | |
| return None | |
| # ------------------------------------------------------------------ # | |
| # Tree utilities | |
| # ------------------------------------------------------------------ # | |
| def _count_nodes(self, node: ReasoningTrace) -> int: | |
| return 1 + sum(self._count_nodes(s) for s in node.sub_questions) | |
| def _depth(self, node: ReasoningTrace) -> int: | |
| if not node.sub_questions: return 1 | |
| return 1 + max(self._depth(s) for s in node.sub_questions) | |
| def _format_tree(self, node: ReasoningTrace, indent: int = 0) -> str: | |
| prefix = " " * indent | |
| ans = node.answer or "?" | |
| mark = "✓" if not node.failed else "✗" | |
| strat = f"[{node.decomposition_strategy}] " if node.decomposition_strategy else "" | |
| line = f"{prefix}{mark} {strat}Q: {node.question!r}\n{prefix} A: {ans!r}\n" | |
| for sub in node.sub_questions: | |
| line += self._format_tree(sub, indent + 1) | |
| return line | |
| def stats(self) -> Dict[str, Any]: | |
| return { | |
| "n_traces_stored": len(self.traces), | |
| "max_depth_seen": max((self._depth(t) for t in self.traces), default=0), | |
| "strategies_used": list(set(t.decomposition_strategy for t in self.traces)), | |
| } | |