"""CBT-18.95M: character-level Transformer with Decoupled 2D-RoPE. Quick start: from cbt import load, add, chat model, tok = load() # local folder or the Hugging Face repo add(model, tok, 123456789, 987654321) # {'answer': '1111111110', 'correct': True, ...} chat(model, tok, "What is the weather in Tokyo?") twin, _ = load(variant="twin_1d") # matched 1D-RoPE control (same architecture, data, steps) """ import json import os from dataclasses import fields from typing import Any, Callable, Dict, Optional, Tuple, Union import torch from .model import ScaledGPT as CBT, ScaledGPTConfig as CBTConfig from .tokenizer import ScaledTokenizer as CBTTokenizer from .tools import execute_tool __all__ = ["CBT", "CBTConfig", "CBTTokenizer", "load", "add", "chat", "execute_tool"] REPO_ID = "Neithanvarvas/CBT" VARIANTS: Dict[str, str] = {"cbt_2d": "", "twin_1d": "twin_1d/"} _PKG_DIR = os.path.dirname(os.path.abspath(__file__)) def _resolve(source: Optional[str], filename: str) -> str: """Finds `filename` in a local folder (default: the repo root next to this package) or downloads it.""" folder = source or os.path.dirname(_PKG_DIR) local = os.path.join(folder, filename) # A clone made without git-lfs leaves a ~130-byte pointer file instead of the weights. if os.path.isfile(local) and not (filename.endswith(".safetensors") and os.path.getsize(local) < 1024): return local from huggingface_hub import hf_hub_download return hf_hub_download(repo_id=source if source and not os.path.isdir(source) else REPO_ID, filename=filename) def load( source: Optional[str] = None, device: Optional[Union[str, torch.device]] = None, variant: str = "cbt_2d", ) -> Tuple[CBT, CBTTokenizer]: """Loads model and tokenizer from a local folder or a Hugging Face repo id; returns (model, tokenizer) in eval mode. variant: "cbt_2d" (the released model) or "twin_1d" (its 1D-RoPE control). """ from safetensors.torch import load_file if variant not in VARIANTS: raise ValueError(f"variant must be one of {sorted(VARIANTS)}") prefix = VARIANTS[variant] with open(_resolve(source, prefix + "config.json"), encoding="utf-8") as f: raw: Dict[str, Any] = json.load(f) config = CBTConfig(**{k.name: raw[k.name] for k in fields(CBTConfig) if k.name in raw}) device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu")) tok = CBTTokenizer() model = CBT(config, tokenizer=tok) model.load_state_dict(load_file(_resolve(source, prefix + "model.safetensors"))) return model.to(device).eval(), tok @torch.no_grad() def _generate(model: CBT, ids: torch.Tensor, max_new_tokens: int, stop: list) -> torch.Tensor: return model.generate(ids, max_new_tokens=max_new_tokens, temperature=0.0, stop_tokens=stop) def _encode(model: CBT, tok: CBTTokenizer, text: str, bos: bool = True) -> torch.Tensor: device = next(model.parameters()).device return torch.tensor([tok.encode(text, add_bos=bos, add_eos=False)], dtype=torch.long, device=device) def add(model: CBT, tok: CBTTokenizer, a: Union[int, str], b: Union[int, str]) -> Dict[str, Any]: """Greedy-decodes a + b with the column scratchpad. Trained on 1-25 digit operands.""" a, b = str(a).strip(), str(b).strip() if not (a.isdigit() and b.isdigit()): raise ValueError("operands must be non-negative integers") budget = 12 * (max(len(a), len(b)) + 2) + 8 out = _generate(model, _encode(model, tok, f"{a}+{b}="), budget, [tok.eos_id]) raw = tok.decode(out[0].tolist()) think, _, tail = raw.partition("")[2].partition("") answer = tail.lstrip("=").replace("", "").strip() return {"answer": answer, "scratchpad": think, "correct": answer == str(int(a) + int(b)), "raw": raw} def chat( model: CBT, tok: CBTTokenizer, message: str, tool_executor: Optional[Callable[[str], str]] = execute_tool, max_new_tokens: int = 256, ) -> Dict[str, Any]: """Runs one user turn: reasoning, optional executed by `tool_executor`, then .""" ids = _encode(model, tok, f"{message}") out = _generate(model, ids, max_new_tokens, [tok.call_e_id, tok.answer_e_id, tok.eos_id]) raw = tok.decode(out[0].tolist()) if "" in raw and tool_executor is not None: call = raw.split("")[-1].split("")[0] cont = torch.cat([out, _encode(model, tok, f"{tool_executor(call)}", bos=False)], dim=1) out = _generate(model, cont, max_new_tokens, [tok.answer_e_id, tok.eos_id]) raw = tok.decode(out[0].tolist()) def span(tag: str) -> Optional[str]: s, e = f"<{tag}>", f"" return raw.split(s, 1)[1].split(e, 1)[0].strip() if s in raw and e in raw else None return {"think": span("THINK"), "call": span("CALL"), "result": span("RESULT"), "answer": span("ANSWER"), "raw": raw}