THX-02 / thx02 /runtime.py
doofz's picture
Add the thx02 PyPI package: API client, CLI (Termux, tmux), lazy local runtime
8262da0 verified
Raw History Blame Contribute Delete
7.62 kB
"""THX-02 runtime: one decision call with option-order averaging, the joint-read rule and self-gated thinking.
from thx02 import THX02
m = THX02.load("HAL-X-AI/THX-02", thinker_url="http://127.0.0.1:8121") # or a local folder; thinker optional
m.decide("I was charged twice, please refund", {"refund": {"type": "noul", "instructions": "Refund request?"}})
The decision logic is a generator (`plan`) that yields batches of sub-requests and thought jobs; `THX02.decide` drives
it synchronously and `thx02.serve` drives many of them concurrently through one batched forward pass.
"""
from __future__ import annotations
import concurrent.futures
import json
import sys
import time
import urllib.request
from pathlib import Path
from .engine import (ALWAYS, AUTO, THINK_SYSTEM, distribution, facet_joint, gate_request, geo_mean, orders, permuted,
think_body, with_thought)
from .protocol import answer, options
def resolve(path) -> Path:
"""A local folder as is; otherwise a Hugging Face repo id, downloaded without the thinker and mobile files."""
local = Path(str(path)).expanduser()
if local.exists():
return local
from huggingface_hub import snapshot_download
return Path(snapshot_download(str(path), ignore_patterns=["thinker/*", "mobile/*", "assets/*"]))
def load_decider(path, device="cuda", fast=False):
if device == "cpu" and "transformers.models.qwen3_5.modeling_qwen3_5" not in sys.modules:
sys.modules.setdefault("fla", None) # GPU-only Triton kernels; use the PyTorch linear-attention path on CPU
from .model import THXDecisionModel
return THXDecisionModel.from_pretrained(resolve(path), device=device, fast=fast)
def plan(req: dict, *, think_available: bool, default_tta: int = 1, max_questions: int = 64):
"""Generator: yields ("decide", [requests]) or ("think", [single-question requests]); receives the outputs
(decision responses or thought strings) and returns the response dict."""
state, questions = req.get("state", ""), req.get("questions")
if not isinstance(questions, dict) or not questions or len(questions) > max_questions:
raise ValueError(f"questions must be an object with 1 to {max_questions} entries")
images = [i for i in (req.get("images") or []) if isinstance(i, str)][:4]
base = {"state": state, **({"images": images} if images else {})}
think = req.get("think", False)
cfg = ALWAYS if think is True else AUTO if think == "auto" else None
if not think_available:
cfg = None
k = max(1, min(int(req.get("tta", default_tta)), 8))
keys = list(questions)
if not images and facet_joint(questions): # facet tagging: one joint read
out, = yield ("decide", [{**base, "questions": questions}])
return {"model": "THX-02", "answers": out["answers"]}
subs, spans = [], []
for key in keys:
q = questions[key]
crit = q.get("criteria") if q.get("type") == "choice" else None
vs = orders(len(crit), k) if k > 1 and isinstance(crit, dict) and len(crit) > 1 else [None]
s = len(subs)
subs += [{**base, "questions": {key: q if o is None else permuted(q, o)}} for o in vs]
spans.append((s, len(subs)))
n_main = len(subs)
if cfg and cfg["gate"] > 0:
subs += [gate_request(state, questions[key]) for key in keys]
outs = yield ("decide", subs)
answers, p0 = {}, {}
for key, (s, e) in zip(keys, spans):
ks = options(questions[key])[0]
ps = [distribution(outs[j]["answers"][key], ks) for j in range(s, e)]
p0[key] = ps[0] if len(ps) == 1 else geo_mean(ps)
answers[key] = outs[s]["answers"][key] if len(ps) == 1 else answer(questions[key], p0[key])
if cfg:
gates = [o["answers"]["g"]["noul"] for o in outs[n_main:]] if cfg["gate"] > 0 else [1.0] * len(keys)
unsure = [key for key, g in zip(keys, gates) if max(p0[key]) < cfg["tau"] and g >= cfg["gate"]]
if unsure:
thoughts = yield ("think", [{**base, "questions": {key: questions[key]}} for key in unsure], cfg["cap"])
pairs = [(key, th) for key, th in zip(unsure, thoughts) if th]
if pairs:
re = yield ("decide", [{**base, "questions": {key: with_thought(questions[key], th)}} for key, th in pairs])
for (key, th), o in zip(pairs, re):
if isinstance(o, Exception):
continue
p1 = distribution(o["answers"][key], options(questions[key])[0])
answers[key] = answer(questions[key], [cfg["alpha"] * x + (1 - cfg["alpha"]) * y for x, y in zip(p0[key], p1)])
if req.get("return_thoughts"):
answers[key]["thought"] = th
return {"model": "THX-02", "answers": answers}
def thinker_payload(enc, request: dict, cap: int) -> dict:
content = [{"type": "image_url", "image_url": {"url": u}} for u in request.get("images", [])]
content.append({"type": "text", "text": think_body(enc, request)})
return {"model": "thx02-thinker", "messages": [{"role": "system", "content": THINK_SYSTEM}, {"role": "user", "content": content}],
"max_tokens": cap, "temperature": 0, "stop": ["</think>"], "chat_template_kwargs": {"enable_thinking": True}}
def thought_text(response: dict) -> str:
m = response["choices"][0]["message"]
return ((m.get("reasoning_content") or m.get("content") or "").replace("<think>", "")).strip()
class THX02:
"""Local THX-02: decisions in-process; thoughts from a vLLM thinker server when thinker_url is set."""
def __init__(self, decider, thinker_url: str | None = None, default_tta: int = 1):
self.d, self.thinker_url, self.default_tta = decider, thinker_url, default_tta
@classmethod
def load(cls, path="HAL-X-AI/THX-02", device="cuda", fast=False, thinker_url=None, default_tta=1):
"""path: a local THX-02 folder, or a Hugging Face repo id (downloaded once, about 9 GB)."""
return cls(load_decider(path, device=device, fast=fast), thinker_url, default_tta)
def _thoughts(self, requests, cap):
def one(r):
try:
body = json.dumps(thinker_payload(self.d.encoder, r, cap)).encode()
q = urllib.request.Request(self.thinker_url.rstrip("/") + "/v1/chat/completions", body, {"Content-Type": "application/json"})
return thought_text(json.load(urllib.request.urlopen(q, timeout=30)))
except Exception: # noqa: BLE001 thinker unavailable: answer directly
return ""
with concurrent.futures.ThreadPoolExecutor(16) as ex:
return list(ex.map(one, requests))
def decide(self, state, questions, images=None, think=False, tta=None, return_thoughts=False) -> dict:
"""think: False | "auto" (only when unsure and reasoning helps) | True (nearly always). tta: option orders (1 or 4)."""
t0 = time.time()
req = {"state": state, "questions": questions, "images": images or [], "think": think,
"tta": self.default_tta if tta is None else tta, "return_thoughts": return_thoughts}
g = plan(req, think_available=bool(self.thinker_url), default_tta=self.default_tta)
msg = next(g)
while True:
try:
msg = g.send(self.d.decide(msg[1]) if msg[0] == "decide" else self._thoughts(msg[1], msg[2]))
except StopIteration as stop:
out = stop.value
out["latency_ms"] = round((time.time() - t0) * 1000, 1)
return out