glyph / app.py
robertkeus's picture
Upload app.py with huggingface_hub
d4d49fc verified
Raw
History Blame Contribute Delete
10.3 kB
"""Glyph — self-contained Hugging Face Space (no training, base model only).
English → base-model intent-extraction → glyph message → deterministic decode →
executed Python. Set GLYPH_MODEL (default 1.5B for CPU responsiveness).
"""
import os
import re
import subprocess
import sys
import tempfile
import time
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
# glyph codepoints (must match glyph/channel.py) → guarantees correct, unique glyphs
_CPS = [0x4e00, 0x4e01, 0x4e03, 0x4e07, 0x4e08, 0x4e09, 0x4e0a, 0x4e0b,
0x4e0d, 0x4e0e, 0x4e0f, 0x4e10, 0x4e11, 0x4e13, 0x4e14, 0x4e15]
_OPS = [ # key, code-line, English description, is_reducer
("evens", "r = [x for x in r if x % 2 == 0]", "keep the even numbers", False),
("pos", "r = [x for x in r if x > 0]", "keep the positive numbers", False),
("double", "r = [x * 2 for x in r]", "double each", False),
("square", "r = [x * x for x in r]", "square each", False),
("inc", "r = [x + 1 for x in r]", "add one to each", False),
("negate", "r = [-x for x in r]", "negate each", False),
("absval", "r = [abs(x) for x in r]", "take the absolute value of each", False),
("rev", "r = list(reversed(r))", "reverse the order", False),
("sorta", "r = sorted(r)", "sort ascending", False),
("sortd", "r = sorted(r, reverse=True)", "sort descending", False),
("uniq", "r = list(dict.fromkeys(r))", "drop duplicates keeping first occurrence", False),
("dec", "r = [x - 1 for x in r]", "subtract one from each", False),
("sum", "return sum(r)", "return their sum", True),
("max", "return max(r) if r else 0", "return the maximum (0 if empty)", True),
("len", "return len(r)", "return how many remain", True),
("cnt", "return sum(1 for _ in r)", "return the count", True),
]
# key, glyph, code-line, description, is_reducer
PRIMS = [(k, chr(_CPS[i]), code, desc, red) for i, (k, code, desc, red) in enumerate(_OPS)]
BY_KEY = {k: p for p in PRIMS for k in [p[0]]}
BY_GLYPH = {p[1]: p for p in PRIMS}
KEYSET = {p[0] for p in PRIMS}
MENU = "\n".join(f"{p[0]} = {p[3]}" for p in PRIMS)
DEMO = [3, -1, 2, 2, -5]
MODEL = os.environ.get("GLYPH_MODEL", "Qwen/Qwen2.5-Coder-0.5B-Instruct") # light → stable on free CPU
DEV = "cuda" if torch.cuda.is_available() else "cpu"
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(
MODEL, torch_dtype=torch.float16 if DEV == "cuda" else torch.float32).to(DEV)
def ask(prompt, max_new=48):
text = tok.apply_chat_template([{"role": "user", "content": prompt}],
tokenize=False, add_generation_prompt=True)
enc = tok(text, return_tensors="pt").to(DEV)
out = model.generate(**enc, max_new_tokens=max_new, do_sample=False,
pad_token_id=tok.eos_token_id)
return tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True).strip()
def intent(msg):
q = ("List the operations the request performs, IN ORDER, as comma-separated "
"keys from this menu (keys only, e.g. `evens, double`):\n" + MENU +
f"\n\nRequest: {msg}\nKeys:")
return [k for t in re.split(r"[,\s]+", ask(q, 32)) if (k := t.strip()) in KEYSET]
def to_code(keys):
lines = ["def solve(xs):", " r = list(xs)"]
for k in keys:
_, _, line, _, red = BY_KEY[k]
lines.append(" " + line)
if red:
return "\n".join(lines)
return "\n".join(lines + [" return r"])
JS = { # same primitives rendered as JavaScript — symbols carry intent, not syntax
"evens": "r = r.filter(x => x % 2 === 0);", "pos": "r = r.filter(x => x > 0);",
"double": "r = r.map(x => x * 2);", "square": "r = r.map(x => x * x);",
"inc": "r = r.map(x => x + 1);", "negate": "r = r.map(x => -x);",
"absval": "r = r.map(x => Math.abs(x));", "rev": "r = r.slice().reverse();",
"sorta": "r = r.slice().sort((a, b) => a - b);", "sortd": "r = r.slice().sort((a, b) => b - a);",
"uniq": "r = [...new Set(r)];", "dec": "r = r.map(x => x - 1);",
"sum": "return r.reduce((a, b) => a + b, 0);", "max": "return r.length ? Math.max(...r) : 0;",
"len": "return r.length;", "cnt": "return r.length;",
}
def to_js(keys):
lines = ["function solve(xs) {", " let r = [...xs];"]
for k in keys:
lines.append(" " + JS[k])
if JS[k].startswith("return"):
return "\n".join(lines + ["}"])
return "\n".join(lines + [" return r;", "}"])
def run(code):
src = f"{code}\n\nprint(solve({DEMO}))\n"
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f:
f.write(src); path = f.name
try:
p = subprocess.run([sys.executable, "-I", path], capture_output=True, text=True, timeout=5)
return p.stdout.strip() if p.returncode == 0 else "error"
except Exception:
return "error"
finally:
os.unlink(path)
KW = { # reliable keyword parse (fast, instant) — order in the sentence = op order
"evens": ["even"], "pos": ["positive"], "double": ["double", "twice"],
"square": ["square"], "inc": ["add one", "increment", "plus one"],
"negate": ["negate", "flip sign", "flip the sign", "invert"],
"absval": ["absolute", "abs value"], "rev": ["revers", "backward", "back to front"],
"sorta": ["ascending", "smallest to", "low to high", "increasing"],
"sortd": ["descending", "big to small", "large to small", "high to low", "biggest to"],
"uniq": ["dedup", "duplicate", "unique", "distinct"],
"dec": ["subtract one", "decrement", "minus one"],
"sum": ["sum", "add up", "total", "add everything", "add them"],
"max": ["max", "biggest", "largest", "top one", "the top", "highest"],
"len": ["how many", "length", "size"], "cnt": ["count", "number of"],
}
def kw_parse(msg):
low = msg.lower()
hits = []
for k, words in KW.items():
for w in words:
i = low.find(w)
if i >= 0:
hits.append((i, k)); break
hits.sort()
seen, keys = set(), []
for _, k in hits:
if k not in seen:
seen.add(k); keys.append(k)
return keys
def _gen(conv):
text = tok.apply_chat_template(conv, tokenize=False, add_generation_prompt=True)
enc = tok(text, return_tensors="pt").to(DEV)
with torch.no_grad():
out = model.generate(**enc, max_new_tokens=120, do_sample=True, temperature=0.7,
pad_token_id=tok.eos_token_id)
return tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True).strip()
def _conv(history, msg):
"""Coerce any Gradio history shape (dicts / tuples / stray types) to a clean
list of {role, content} string pairs."""
conv = []
for h in (history or [])[-6:]:
if isinstance(h, dict):
role, content = h.get("role"), h.get("content")
if role in ("user", "assistant") and isinstance(content, str) and content.strip():
conv.append({"role": role, "content": content})
elif isinstance(h, (list, tuple)) and len(h) == 2:
u, a = h
if isinstance(u, str) and u.strip():
conv.append({"role": "user", "content": u})
if isinstance(a, str) and a.strip():
conv.append({"role": "assistant", "content": a})
conv.append({"role": "user", "content": msg})
return conv
def _chat(history, msg):
"""Plain generation. Falls back to no-history if the history shape is odd."""
try:
return _gen(_conv(history, msg))
except Exception:
return _gen([{"role": "user", "content": msg}])
def respond(msg, history):
"""Generator → streams a visible chain of thought (Gradio renders each yield)."""
try:
yield from _respond(msg, history)
except Exception as e: # never hard-crash the UI
yield f"⚠️ hiccup: {type(e).__name__}: {str(e)[:150]}"
def _respond(msg, history):
msg = (msg or "").strip()
if not msg:
yield "Ask me anything — for list-of-number tasks I show my reasoning in glyphs + code."
return
if all(c in BY_GLYPH for c in msg):
keys = [BY_GLYPH[c][0] for c in msg]
else:
keys = kw_parse(msg) # reliable; no model guessing (avoids false glyph mode on chat)
if keys: # stream the glyph reasoning step by step
al = lambda g: chr(0x1400 + (ord(g) - 0x4e00))
buf = "🧠 *reading your request…*"
yield buf
time.sleep(0.5)
buf = "🧠 **Reasoning — turning your words into my glyph language:**\n\n"
yield buf
for i, k in enumerate(keys):
time.sleep(0.6)
buf += f"{i+1}. *{BY_KEY[k][3]}* → **{al(BY_KEY[k][1])}** → `{BY_KEY[k][2]}`\n"
yield buf
glyphs = "".join(al(BY_KEY[k][1]) for k in keys)
cut = round((1 - len(keys) * 2 / max(len(msg), 1)) * 100)
time.sleep(0.5)
buf += (f"\n📨 **Message the agents send:** {glyphs} · **{len(keys)*2} bytes** "
f"vs {len(msg)} in English (**{cut}% smaller**)\n")
yield buf
code = to_code(keys)
time.sleep(0.4)
buf += f"\n🛠️ **Composing the code…**\n```python\n{code}\n```\n"
yield buf
time.sleep(0.4)
buf += (f"\n🌐 **Same message, different language** (the glyphs encode intent, "
f"not Python):\n```javascript\n{to_js(keys)}\n```\n")
yield buf
time.sleep(0.5)
buf += f"\n▶️ **Running it:** `solve({DEMO})` → **`{run(code)}`**"
yield buf
return
yield _chat(history or [], msg) # normal chat (non-threaded, robust)
EXAMPLES = ["Hi, what can you do?", "keep positives, square them, then sum",
"sort big to small and give the top one", "remove duplicates then add everything up",
"explain your glyph language"]
gr.ChatInterface(
respond, examples=EXAMPLES, title="Glyph",
description="A chat model that speaks its own compact glyph language for list-of-number "
"tasks — answering in symbols, then real Python. Chat normally too.",
).launch()