nanoGenzen-GUI / app.py
Sagicc's picture
Update app.py
f145385 verified
Raw
History Blame Contribute Delete
13.5 kB
import os
import re
import time
from threading import Thread
from typing import Any, Dict, Generator, List, Tuple
import gradio as gr
import spaces
import torch
from safetensors.torch import load_file
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
from nanogentzen.kernel import Sequent, verify_proof_tree
from nanogentzen.model import GentzenPolicyValueNet, PolicyValueConfig
from nanogentzen.parser import parse_natural_language, parse_symbolic_sequent
from nanogentzen.search import NeuralProofSearch
from nanogentzen.tokenizer import LogicTokenizer
# =====================================================================
# 1. LOAD LOCAL LLM (SYSTEM 1)
# =====================================================================
MODEL_ID = "Qwen/Qwen3.5-4B"
print(f"[*] Loading System 1 LLM: {MODEL_ID}...")
llm_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
llm_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
device_map="auto",
)
llm_model.eval()
print("[+] System 1 loaded successfully.")
# =====================================================================
# 2. LOAD NANOGENTZEN KERNEL (SYSTEM 2)
# =====================================================================
def render_tree(node: dict, indent: int = 0) -> str:
space = " " * indent
rule = node.get("rule", "UNKNOWN")
seq = node.get("sequent", "")
res = f"{space}β€’ [{rule}] {seq}\n"
for child in node.get("branches", []):
res += render_tree(child, indent + 1)
return res
def load_gentzen_engine():
device = "cpu"
tokenizer = LogicTokenizer()
config = PolicyValueConfig(vocab_size=tokenizer.vocab_size)
weights_file = (
"nanogentzen_model.safetensors"
if os.path.exists("nanogentzen_model.safetensors")
else "model.safetensors"
)
if not os.path.exists(weights_file):
raise FileNotFoundError(f"Missing weights file: {weights_file}")
model = GentzenPolicyValueNet(config).to(device)
model.load_state_dict(load_file(weights_file, device=device))
model.eval()
return NeuralProofSearch(model, tokenizer, device=device)
searcher = load_gentzen_engine()
def prove_comprehensive(query_str: str, max_depth: int = 8) -> Dict[str, Any]:
seq = parse_symbolic_sequent(query_str)
desc = "Formal symbolic sequent"
if seq is None:
nl_res = parse_natural_language(query_str)
if nl_res:
seq, desc = nl_res
if seq is None:
return {"success": False, "error": "Unable to parse into a valid sequent Ξ“ ⊒ Ξ”."}
# 1. Intuitionistic Logic (LI)
t0 = time.perf_counter()
li_tree = searcher.prove(seq, max_depth=max_depth)
li_ms = round((time.perf_counter() - t0) * 1000.0, 2)
li_sound = li_tree is not None and verify_proof_tree(li_tree)
# 2. Classical Logic (LK via Glivenko Translation: Gamma |- ~~Delta)
gamma_str = ", ".join(f.to_str() for f in seq.gamma) if seq.gamma else "0"
delta_str = seq.delta[0].to_str() if seq.delta else "0"
glivenko_seq = parse_symbolic_sequent(f"{gamma_str} |- ~~({delta_str})")
t1 = time.perf_counter()
lk_tree = searcher.prove(glivenko_seq, max_depth=max_depth) if glivenko_seq else None
lk_ms = round((time.perf_counter() - t1) * 1000.0, 2)
lk_sound = lk_tree is not None and verify_proof_tree(lk_tree)
return {
"success": True,
"description": desc,
"sequent": seq.to_str(),
"li_proven": li_sound,
"li_derivation": render_tree(li_tree) if li_sound else None,
"li_latency_ms": li_ms,
"lk_proven": lk_sound,
"lk_derivation": render_tree(lk_tree) if lk_sound else None,
"lk_latency_ms": lk_ms,
}
def audit_neurosymbolic(think_text: str, user_input: str, max_depth: int = 8) -> Dict[str, Any]:
has_logic = bool(re.search(r"(\|-|⟢|⊒|=>|&|~|\||\bif\b|\bthen\b|\btherefore\b)", user_input.lower()))
proof_res = None
if think_text:
seq_match = re.search(
r"(?:Formal Sequent:?\s*|Sequent:?\s*)?([A-Za-z0-9_~\(\)\s,=&|=>\-\>⟹∧∨¬]+(\|-|⟢|⊒)[A-Za-z0-9_~\(\)\s,=&|=>\-\>⟹∧∨¬]+)",
think_text,
re.IGNORECASE,
)
if seq_match:
cand = seq_match.group(1).split("\n")[0].strip()
alt = prove_comprehensive(cand, max_depth=max_depth)
if alt.get("success"):
proof_res = alt
if (not proof_res or not proof_res.get("success")) and has_logic:
nl_res = prove_comprehensive(user_input, max_depth=max_depth)
if nl_res.get("success"):
proof_res = nl_res
if proof_res and proof_res.get("success"):
proof_res["is_formal_proof"] = True
return proof_res
return {
"success": True,
"is_formal_proof": False,
"li_proven": False,
"lk_proven": False,
"li_latency_ms": 1.5,
"lk_latency_ms": 1.5,
}
# =====================================================================
# 3. CHAT STREAMING PIPELINE (ZEROGPU)
# =====================================================================
SYSTEM_PROMPT = """You are an advanced Neurosymbolic AI Assistant.
Always start your response with a transparent, structured <think> block:
<think>
1. Problem Deconstruction: Identify premises and core goal.
2. Formal Sequent: If testing a deduction, write the symbolic sequent:
Formal Sequent: (P => Q), P |- Q
3. Step-by-Step Analysis: Verify implications and rule applications.
</think>
After </think>, deliver a direct, comprehensive explanation."""
@spaces.GPU(duration=60)
def chat_stream(
message: str,
history: List[Dict[str, str]],
temperature: float,
max_tokens: int,
max_depth: int,
) -> Generator[List[Dict[str, str]], None, None]:
# 1. Direct formal sequent check
has_turnstile = bool(re.search(r"(\|-|⟢|⊒)", message))
if has_turnstile:
res = prove_comprehensive(message, max_depth=int(max_depth))
if res.get("success"):
derivation = f"\n```text\n{res['li_derivation']}```" if res.get("li_derivation") else ""
response = (
f"### πŸ›‘οΈ Sequent Certificate: `{res['sequent']}`\n\n"
f"- **Intuitionistic Status (LI)**: {'βœ… **PROVEN (Sound)**' if res['li_proven'] else '❌ **UNPROVABLE**'} (`{res['li_latency_ms']}ms`)\n"
f"{derivation}\n"
f"- **Classical Status (LK)**: {'βœ… **VALID (Classical Tautology)**' if res['lk_proven'] else '❌ **INVALID**'} (`{res['lk_latency_ms']}ms`)"
)
else:
response = f"❌ **Syntax / Parse Error**: {res.get('error')}"
history.append({"role": "user", "content": message})
history.append({"role": "assistant", "content": response})
yield history
return
# 2. Local Transformers Generation
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
for msg in history:
messages.append({"role": msg["role"], "content": msg["content"]})
messages.append({"role": "user", "content": message})
prompt_text = llm_tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
model_inputs = llm_tokenizer([prompt_text], return_tensors="pt").to(llm_model.device)
streamer = TextIteratorStreamer(
llm_tokenizer,
timeout=40.0,
skip_prompt=True,
skip_special_tokens=True,
)
generate_kwargs = dict(
model_inputs,
streamer=streamer,
max_new_tokens=int(max_tokens),
temperature=float(temperature) if temperature > 0.0 else None,
do_sample=temperature > 0.0,
pad_token_id=llm_tokenizer.eos_token_id,
)
thread = Thread(target=llm_model.generate, kwargs=generate_kwargs)
thread.start()
history.append({"role": "user", "content": message})
history.append({"role": "assistant", "content": ""})
accumulated = ""
for new_text in streamer:
accumulated += new_text
history[-1]["content"] = accumulated
yield history
# Extract thought trace and trigger System 2 verification
think_body = ""
if "<think>" in accumulated and "</think>" in accumulated:
think_body = accumulated.split("</think>")[0].replace("<think>", "").strip()
audit = audit_neurosymbolic(think_body, message, max_depth=int(max_depth))
if audit.get("is_formal_proof") and audit.get("li_proven"):
badge = f"\n\n---\n**⚑ nanoGentzen Audit (`{audit['li_latency_ms']}ms`):** βœ… `PROVEN SOUND (Q.E.D.)`\n"
if audit.get("li_derivation"):
badge += f"```text\n{audit['li_derivation']}```"
accumulated += badge
elif audit.get("is_formal_proof") and audit.get("lk_proven"):
accumulated += f"\n\n---\n**⚑ nanoGentzen Audit (`{audit['lk_latency_ms']}ms`):** πŸ›οΈ `CLASSICAL TAUTOLOGY (LK via Glivenko)`"
elif audit.get("is_formal_proof") and not audit.get("li_proven") and not audit.get("lk_proven"):
accumulated += f"\n\n---\n**⚑ nanoGentzen Audit (`{audit['li_latency_ms']}ms`):** ⚠️ `FALLACY DETECTED (Pruned Non-Sequitur)`"
history[-1]["content"] = accumulated
yield history
def prove_lab(sequent_str: str, max_depth: int) -> Tuple[str, str, str, str]:
res = prove_comprehensive(sequent_str, max_depth=int(max_depth))
if not res.get("success"):
return "❌ Error", res.get("error", "Parse error"), "❌ Error", "Parse error"
li_status = f"{'βœ… PROVEN SOUND' if res['li_proven'] else '❌ UNPROVABLE'} ({res['li_latency_ms']}ms)"
li_tree = res.get("li_derivation") or "No constructive proof tree."
lk_status = f"{'βœ… CLASSICAL TAUTOLOGY' if res['lk_proven'] else '❌ INVALID'} ({res['lk_latency_ms']}ms)"
lk_tree = res.get("lk_derivation") or "Counter-model exists in Boolean valuation."
return li_status, li_tree, lk_status, lk_tree
# =====================================================================
# 4. GRADIO LAYOUT
# =====================================================================
with gr.Blocks() as demo:
gr.Markdown("# 🧠 nanoGentzen Neurosymbolic Studio")
gr.Markdown(
f"**System 1 (`{MODEL_ID}` on ZeroGPU)** generates transparent `<think>` traces, while **System 2 (nanoGentzen)** certifies mathematical soundness in < 25 ms."
)
with gr.Tabs():
with gr.Tab("πŸ’¬ Neurosymbolic Chat"):
chatbot = gr.Chatbot(height=520)
with gr.Row():
msg_input = gr.Textbox(
placeholder="Ask a logic question or enter a sequent (e.g., '(P => Q), ~Q |- ~P')...",
show_label=False,
scale=8,
)
submit_btn = gr.Button("Send πŸš€", scale=1, variant="primary")
with gr.Accordion("βš™οΈ Generation Hyperparameters", open=False):
with gr.Row():
temp_slide = gr.Slider(0.0, 1.0, value=0.2, step=0.05, label="Temperature")
token_slide = gr.Slider(256, 2048, value=1024, step=128, label="Max Tokens")
depth_slide = gr.Slider(4, 16, value=8, step=1, label="nanoGentzen Max Depth")
gr.Examples(
examples=[
["If it rains, the street is wet. The street is not wet. Did it rain?"],
["((P => Q) => P) |- P"],
["(P => Q), Q |- P"],
["0 |- ~~ (~~P => P)"],
],
inputs=msg_input,
)
submit_btn.click(
chat_stream,
inputs=[msg_input, chatbot, temp_slide, token_slide, depth_slide],
outputs=[chatbot],
).then(lambda: "", None, msg_input)
msg_input.submit(
chat_stream,
inputs=[msg_input, chatbot, temp_slide, token_slide, depth_slide],
outputs=[chatbot],
).then(lambda: "", None, msg_input)
with gr.Tab("πŸ”¬ Dual-Mode Logic Prover Lab"):
gr.Markdown("### Interactive Sequent Verification ($LI$ vs $LK$)")
with gr.Row():
prover_input = gr.Textbox(
value="(P => Q), ~Q |- ~P",
label="Enter Sequent Ξ“ ⊒ Ξ”",
scale=4,
)
prove_depth = gr.Slider(4, 16, value=8, step=1, label="Max Depth", scale=1)
prove_btn = gr.Button("⚑ Prove Sequent", variant="primary", scale=1)
with gr.Row():
with gr.Column():
gr.Markdown("#### 🌿 Intuitionistic Logic (LI)")
li_status_out = gr.Textbox(label="Status", interactive=False)
li_tree_out = gr.Code(label="Derivation Tree", language="markdown")
with gr.Column():
gr.Markdown("#### πŸ›οΈ Classical Logic (LK via Glivenko)")
lk_status_out = gr.Textbox(label="Status", interactive=False)
lk_tree_out = gr.Code(label="Derivation Tree", language="markdown")
prove_btn.click(
prove_lab,
inputs=[prover_input, prove_depth],
outputs=[li_status_out, li_tree_out, lk_status_out, lk_tree_out],
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Soft(primary_hue="indigo"))