Update app.py
Browse files
app.py
CHANGED
|
@@ -2,12 +2,11 @@ import os
|
|
| 2 |
import re
|
| 3 |
import json
|
| 4 |
import time
|
| 5 |
-
import shutil
|
| 6 |
import torch
|
| 7 |
import torch.nn as nn
|
| 8 |
import gradio as gr
|
| 9 |
from pathlib import Path
|
| 10 |
-
from collections import deque
|
| 11 |
|
| 12 |
# ─────────────────────────────────────────
|
| 13 |
# 🔐 Admin password
|
|
@@ -23,7 +22,6 @@ CONFIG_PATH = SPACE_ROOT / "config.json"
|
|
| 23 |
_pt_files = sorted(SPACE_ROOT.glob("*.pt"))
|
| 24 |
MODEL_PATH = _pt_files[0] if _pt_files else SPACE_ROOT / "default_model.pt"
|
| 25 |
|
| 26 |
-
# Find tokenizer — any .json that isn't config.json
|
| 27 |
_tok_files = [f for f in SPACE_ROOT.glob("*.json") if f.name != "config.json"]
|
| 28 |
TOKENIZER_PATH = _tok_files[0] if _tok_files else None
|
| 29 |
|
|
@@ -31,7 +29,7 @@ TOKENIZER_PATH = _tok_files[0] if _tok_files else None
|
|
| 31 |
# 🗃️ Config
|
| 32 |
# ─────────────────────────────────────────
|
| 33 |
DEFAULT_CONFIG = {
|
| 34 |
-
"model_type": "char",
|
| 35 |
"hidden_layers": 5,
|
| 36 |
"neurons": 768,
|
| 37 |
"embed_size": 384,
|
|
@@ -42,17 +40,16 @@ DEFAULT_CONFIG = {
|
|
| 42 |
"bot_tag": "### Response:",
|
| 43 |
"eos_token": "<|end|>",
|
| 44 |
"system_prompt": "You are a helpful and intelligent AI assistant named Linny.",
|
| 45 |
-
"default_temp": 0.
|
| 46 |
-
"default_penalty": 1.
|
| 47 |
-
"default_penalty_window": 110,
|
| 48 |
-
"default_top_p": 0.4,
|
| 49 |
-
"default_top_k": 65,
|
| 50 |
-
"default_max_len":
|
| 51 |
-
"
|
| 52 |
-
"
|
| 53 |
-
|
| 54 |
-
"
|
| 55 |
-
"reasoning_start": True,
|
| 56 |
}
|
| 57 |
|
| 58 |
def load_config() -> dict:
|
|
@@ -68,7 +65,6 @@ def save_config(cfg: dict):
|
|
| 68 |
with open(CONFIG_PATH, "w") as f:
|
| 69 |
json.dump(cfg, f, indent=2)
|
| 70 |
|
| 71 |
-
|
| 72 |
# ─────────────────────────────────────────
|
| 73 |
# 🧠 Model Architectures (unchanged)
|
| 74 |
# ─────────────────────────────────────────
|
|
@@ -77,32 +73,25 @@ class LSTMCharLM(nn.Module):
|
|
| 77 |
super().__init__()
|
| 78 |
self.embed = nn.Embedding(vocab_size, embed_size)
|
| 79 |
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers=num_layers,
|
| 80 |
-
batch_first=True,
|
| 81 |
-
|
| 82 |
-
self.fc = nn.Linear(hidden_size, vocab_size)
|
| 83 |
-
|
| 84 |
def forward(self, x, hidden=None):
|
| 85 |
-
|
| 86 |
-
out, hidden = self.lstm(e, hidden)
|
| 87 |
return self.fc(out), hidden
|
| 88 |
|
| 89 |
-
|
| 90 |
class LSTMTokenLM(nn.Module):
|
| 91 |
def __init__(self, vocab_size, embed_size, hidden_size, num_layers, dropout=0.2):
|
| 92 |
super().__init__()
|
| 93 |
self.embed = nn.Embedding(vocab_size, embed_size)
|
| 94 |
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers=num_layers,
|
| 95 |
-
batch_first=True,
|
| 96 |
-
|
| 97 |
-
self.fc = nn.Linear(hidden_size, vocab_size)
|
| 98 |
-
|
| 99 |
def forward(self, x, hidden=None):
|
| 100 |
out, hidden = self.lstm(self.embed(x), hidden)
|
| 101 |
return self.fc(out), hidden
|
| 102 |
|
| 103 |
-
|
| 104 |
# ─────────────────────────────────────────
|
| 105 |
-
# 🔤 GPT-2 byte decoder
|
| 106 |
# ─────────────────────────────────────────
|
| 107 |
def _build_byte_decoder():
|
| 108 |
bs = (list(range(ord('!'), ord('~')+1)) +
|
|
@@ -113,7 +102,7 @@ def _build_byte_decoder():
|
|
| 113 |
for b in range(256):
|
| 114 |
if b not in bs:
|
| 115 |
bs.append(b)
|
| 116 |
-
cs.append(256
|
| 117 |
n += 1
|
| 118 |
return {chr(c): b for b, c in zip(bs, cs)}
|
| 119 |
|
|
@@ -125,122 +114,60 @@ def _tok_to_bytes(tok_str):
|
|
| 125 |
except KeyError:
|
| 126 |
return tok_str.encode('utf-8', errors='replace')
|
| 127 |
|
| 128 |
-
|
| 129 |
# ─────────────────────────────────────────
|
| 130 |
-
# ⚙️ Model Loader (
|
| 131 |
# ─────────────────────────────────────────
|
| 132 |
class LinnyModel:
|
| 133 |
def __init__(self, pt_path, config: dict, tokenizer_path=None):
|
| 134 |
self.config = config
|
| 135 |
self.model_type = config.get("model_type", "char")
|
| 136 |
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 137 |
-
|
| 138 |
-
# ADDED: Cache for hidden states to speed up repeated prefixes
|
| 139 |
-
self.cached_hidden = None
|
| 140 |
-
self.cached_prefix = None
|
| 141 |
-
self.cached_prefix_ids = None
|
| 142 |
|
| 143 |
ckpt = torch.load(pt_path, map_location=self.device, weights_only=False)
|
| 144 |
|
| 145 |
if self.model_type == "token":
|
| 146 |
-
|
| 147 |
-
from tokenizers import Tokenizer as HFTokenizer
|
| 148 |
-
except ImportError:
|
| 149 |
-
raise ImportError("Install tokenizers: pip install tokenizers")
|
| 150 |
-
|
| 151 |
tok_path = tokenizer_path or config.get("tokenizer_path")
|
| 152 |
if not tok_path or not Path(str(tok_path)).exists():
|
| 153 |
raise FileNotFoundError(f"Tokenizer not found: {tok_path}")
|
| 154 |
-
|
| 155 |
self.tokenizer = HFTokenizer.from_file(str(tok_path))
|
| 156 |
-
vocab_size
|
| 157 |
-
self.chars
|
| 158 |
-
|
| 159 |
-
self.itos = None
|
| 160 |
-
|
| 161 |
-
arch = ckpt.get('config', {})
|
| 162 |
layers = arch.get('hidden_layers', config['hidden_layers'])
|
| 163 |
neurons = arch.get('neurons', config['neurons'])
|
| 164 |
embed = arch.get('embed_size', config['embed_size'])
|
| 165 |
dropout = arch.get('dropout', config.get('dropout', 0.2))
|
| 166 |
-
|
| 167 |
self.model = LSTMTokenLM(vocab_size, embed, neurons, layers, dropout).to(self.device)
|
| 168 |
self.model.load_state_dict(ckpt['model_state'])
|
| 169 |
-
|
| 170 |
else:
|
| 171 |
self.tokenizer = None
|
| 172 |
-
self.chars
|
| 173 |
-
self.stoi
|
| 174 |
-
self.itos
|
| 175 |
-
|
| 176 |
dropout = ckpt.get("config", {}).get("dropout", config.get("dropout", 0.2))
|
| 177 |
-
self.model = LSTMCharLM(
|
| 178 |
-
|
| 179 |
-
config["embed_size"],
|
| 180 |
-
config["neurons"],
|
| 181 |
-
config["hidden_layers"],
|
| 182 |
-
dropout,
|
| 183 |
-
).to(self.device)
|
| 184 |
self.model.load_state_dict(ckpt["model_state"])
|
| 185 |
|
| 186 |
-
# ADDED: Try to use torch.compile for speed (PyTorch 2.0+)
|
| 187 |
-
if hasattr(torch, 'compile') and self.device == torch.device("cpu"):
|
| 188 |
-
try:
|
| 189 |
-
self.model = torch.compile(self.model, mode="reduce-overhead")
|
| 190 |
-
print("✅ TorchScript compilation enabled for CPU speedup")
|
| 191 |
-
except:
|
| 192 |
-
print("⚠️ Torch.compile failed, using regular model")
|
| 193 |
-
|
| 194 |
self.model.eval()
|
| 195 |
self.epoch = ckpt.get('epoch', '?')
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
return hidden, torch.tensor([[ids[-1]]], dtype=torch.long).to(self.device)
|
| 206 |
-
|
| 207 |
-
# ADDED: Method to get cached hidden state for repeated prompts
|
| 208 |
-
def get_cached_prefix(self, prefix_text):
|
| 209 |
-
"""Cache hidden state for a prefix to avoid recomputation"""
|
| 210 |
-
if self.cached_prefix == prefix_text:
|
| 211 |
-
return self.cached_hidden, self.cached_prefix_ids
|
| 212 |
-
|
| 213 |
-
if self.model_type == "token":
|
| 214 |
-
ids = self.tokenizer.encode(prefix_text).ids
|
| 215 |
-
else:
|
| 216 |
-
ids = [self.stoi.get(c, 0) for c in prefix_text]
|
| 217 |
-
|
| 218 |
-
if not ids:
|
| 219 |
-
return None, None
|
| 220 |
-
|
| 221 |
-
t = torch.tensor([ids], dtype=torch.long).to(self.device)
|
| 222 |
-
with torch.no_grad():
|
| 223 |
-
_, hidden = self.model(t, None)
|
| 224 |
-
|
| 225 |
-
self.cached_prefix = prefix_text
|
| 226 |
-
self.cached_hidden = hidden
|
| 227 |
-
self.cached_prefix_ids = torch.tensor([[ids[-1]]], dtype=torch.long).to(self.device)
|
| 228 |
-
|
| 229 |
-
return hidden, self.cached_prefix_ids
|
| 230 |
-
|
| 231 |
-
def stream_generate(self, prompt, temperature=0.7, max_len=1575,
|
| 232 |
-
penalty=1.2, penalty_window=110, # ADDED penalty_window
|
| 233 |
-
top_p=0.9, top_k=50, force_thinking=False,
|
| 234 |
-
prefix_text="", penalize_prefix=False, # ADDED for continue feature
|
| 235 |
-
min_response_tokens=3, # ADDED
|
| 236 |
-
max_reasoning_tokens=2500): # ADDED
|
| 237 |
"""
|
| 238 |
-
|
| 239 |
"""
|
| 240 |
-
cfg
|
| 241 |
user_tag = cfg.get("user_tag", "### Instruction:")
|
| 242 |
-
bot_tag = cfg.get("bot_tag",
|
| 243 |
-
r_mode = cfg.get("reasoning_mode", "
|
| 244 |
eos_token_str = cfg.get("eos_token", "<|end|>")
|
| 245 |
|
| 246 |
# Apply prompt_suffix mode
|
|
@@ -250,32 +177,22 @@ class LinnyModel:
|
|
| 250 |
actual_prompt = prompt.strip() + " /think"
|
| 251 |
|
| 252 |
formatted = f"{user_tag}\n{actual_prompt}\n\n{bot_tag}\n"
|
| 253 |
-
|
|
|
|
| 254 |
working_memory = cfg.get("working_memory", 0)
|
| 255 |
if working_memory > 0:
|
| 256 |
max_len = max(50, min(max_len, working_memory - len(formatted)))
|
| 257 |
|
| 258 |
hidden = None
|
| 259 |
generated = ""
|
| 260 |
-
|
| 261 |
-
# ADDED: Track recent tokens for penalty
|
| 262 |
recent_tokens = deque(maxlen=penalty_window)
|
| 263 |
-
|
| 264 |
-
#
|
| 265 |
in_reasoning = False
|
| 266 |
think_closed = False
|
| 267 |
awaiting_response = False
|
| 268 |
response_token_count = 0
|
| 269 |
reasoning_toks = 0
|
| 270 |
-
|
| 271 |
-
# Get token IDs for special tokens (token mode only)
|
| 272 |
-
eos_id = None
|
| 273 |
-
think_open_id = None
|
| 274 |
-
think_close_id = None
|
| 275 |
-
if self.model_type == "token":
|
| 276 |
-
eos_id = self.tokenizer.token_to_id(eos_token_str)
|
| 277 |
-
think_open_id = self.tokenizer.token_to_id("<think>")
|
| 278 |
-
think_close_id = self.tokenizer.token_to_id("</think>")
|
| 279 |
|
| 280 |
with torch.no_grad():
|
| 281 |
# Encode the conversation prefix
|
|
@@ -284,44 +201,47 @@ class LinnyModel:
|
|
| 284 |
else:
|
| 285 |
ids = [self.stoi.get(c, 0) for c in formatted]
|
| 286 |
|
| 287 |
-
t = torch.tensor([ids], dtype=torch.long
|
| 288 |
_, hidden = self.model(t, hidden)
|
| 289 |
-
input_token = torch.tensor([[ids[-1]]], dtype=torch.long
|
| 290 |
-
|
| 291 |
-
# If we have existing assistant response (
|
| 292 |
if prefix_text:
|
| 293 |
if self.model_type == "token":
|
| 294 |
prefix_ids = self.tokenizer.encode(prefix_text).ids
|
| 295 |
else:
|
| 296 |
prefix_ids = [self.stoi.get(c, 0) for c in prefix_text]
|
| 297 |
-
|
| 298 |
if prefix_ids:
|
| 299 |
-
pt = torch.tensor([prefix_ids], dtype=torch.long
|
| 300 |
_, hidden = self.model(pt, hidden)
|
| 301 |
generated = prefix_text
|
| 302 |
-
input_token = torch.tensor([[prefix_ids[-1]]], dtype=torch.long
|
| 303 |
if penalize_prefix:
|
| 304 |
recent_tokens.extend(prefix_ids)
|
| 305 |
-
|
| 306 |
# Update state based on prefix
|
| 307 |
if "<think>" in prefix_text and "</think>" not in prefix_text:
|
| 308 |
in_reasoning = True
|
| 309 |
-
think_closed = False
|
| 310 |
elif "</think>" in prefix_text:
|
| 311 |
think_closed = True
|
| 312 |
awaiting_response = True
|
| 313 |
|
| 314 |
-
# Prefill <think> if in response_prefix mode
|
| 315 |
if not prefix_text and force_thinking and r_mode == "response_prefix":
|
| 316 |
-
if self.model_type == "token"
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
_, hidden = self.model(tt, hidden)
|
| 319 |
-
input_token =
|
| 320 |
-
generated = "<think>"
|
| 321 |
-
yield "<think>"
|
| 322 |
-
in_reasoning = True
|
| 323 |
-
elif self.model_type == "char":
|
| 324 |
-
hidden, input_token = self._prime_hidden("<think>", hidden)
|
| 325 |
generated = "<think>"
|
| 326 |
for ch in "<think>":
|
| 327 |
yield ch
|
|
@@ -330,25 +250,40 @@ class LinnyModel:
|
|
| 330 |
if cfg.get("reasoning_start", False) and not prefix_text:
|
| 331 |
prefix = f"I need to think about this. The user said '{prompt}'"
|
| 332 |
if self.model_type == "token":
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
| 334 |
generated += prefix
|
| 335 |
yield prefix
|
| 336 |
else:
|
| 337 |
-
hidden, input_token = self._prime_hidden(prefix, hidden)
|
| 338 |
-
generated += prefix
|
| 339 |
for ch in prefix:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
yield ch
|
| 341 |
|
| 342 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
if self.model_type == "token":
|
| 344 |
byte_buf = b""
|
| 345 |
-
token_count = 0
|
| 346 |
-
|
| 347 |
for step in range(max_len):
|
| 348 |
logits, hidden = self.model(input_token, hidden)
|
| 349 |
lf = logits[0, -1].float() / max(temperature, 1e-8)
|
| 350 |
|
| 351 |
-
#
|
| 352 |
if penalty != 1.0 and len(recent_tokens) > 0:
|
| 353 |
penalized_ids = set(recent_tokens)
|
| 354 |
for token_id in penalized_ids:
|
|
@@ -369,16 +304,14 @@ class LinnyModel:
|
|
| 369 |
lf[si[rm]] = float("-inf")
|
| 370 |
|
| 371 |
nxt = torch.multinomial(torch.softmax(lf, dim=-1), 1).item()
|
| 372 |
-
token_count += 1
|
| 373 |
|
| 374 |
# EOS handling with forced minimum response
|
| 375 |
-
if
|
| 376 |
if awaiting_response and response_token_count < min_response_tokens:
|
| 377 |
-
continue
|
| 378 |
else:
|
| 379 |
break
|
| 380 |
|
| 381 |
-
# Update recent tokens for penalty
|
| 382 |
recent_tokens.append(nxt)
|
| 383 |
|
| 384 |
# Update reasoning state
|
|
@@ -390,24 +323,22 @@ class LinnyModel:
|
|
| 390 |
think_closed = True
|
| 391 |
awaiting_response = True
|
| 392 |
response_token_count = 0
|
| 393 |
-
|
| 394 |
if in_reasoning and not think_closed:
|
| 395 |
reasoning_toks += 1
|
| 396 |
-
if max_reasoning_tokens and
|
| 397 |
# Force close think
|
| 398 |
if byte_buf:
|
| 399 |
decoded = byte_buf.decode('utf-8', errors='replace')
|
| 400 |
generated += decoded
|
| 401 |
yield decoded
|
| 402 |
byte_buf = b""
|
| 403 |
-
# Yield closing tag
|
| 404 |
yield "</think>"
|
| 405 |
in_reasoning = False
|
| 406 |
think_closed = True
|
| 407 |
awaiting_response = True
|
| 408 |
response_token_count = 0
|
| 409 |
-
|
| 410 |
-
ct = torch.tensor([[think_close_id]], dtype=torch.long).to(self.device)
|
| 411 |
_, hidden = self.model(ct, hidden)
|
| 412 |
input_token = ct
|
| 413 |
recent_tokens.append(think_close_id)
|
|
@@ -418,7 +349,7 @@ class LinnyModel:
|
|
| 418 |
elif not in_reasoning and not think_closed:
|
| 419 |
response_token_count += 1
|
| 420 |
|
| 421 |
-
#
|
| 422 |
tok_str = self.tokenizer.id_to_token(nxt) or ""
|
| 423 |
byte_buf += _tok_to_bytes(tok_str)
|
| 424 |
try:
|
|
@@ -428,36 +359,31 @@ class LinnyModel:
|
|
| 428 |
byte_buf = b""
|
| 429 |
except UnicodeDecodeError:
|
| 430 |
pass
|
| 431 |
-
|
| 432 |
-
input_token = torch.tensor([[nxt]], dtype=torch.long
|
| 433 |
-
|
| 434 |
if byte_buf:
|
| 435 |
leftover = byte_buf.decode('utf-8', errors='replace')
|
| 436 |
generated += leftover
|
| 437 |
yield leftover
|
| 438 |
-
|
| 439 |
-
# Signal if we hit the token limit (for UI "keep generating" button)
|
| 440 |
-
if token_count >= max_len - 5:
|
| 441 |
-
yield "__HIT_LIMIT__"
|
| 442 |
|
| 443 |
-
#
|
|
|
|
|
|
|
| 444 |
else:
|
| 445 |
-
token_count = 0
|
| 446 |
for step in range(max_len):
|
| 447 |
logits, hidden = self.model(input_token, hidden)
|
| 448 |
lf = logits[0, -1].float() / max(temperature, 1e-8)
|
| 449 |
-
|
| 450 |
-
#
|
| 451 |
if penalty != 1.0 and len(recent_tokens) > 0:
|
| 452 |
-
#
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
else:
|
| 457 |
-
idx = ch
|
| 458 |
if idx < lf.size(0):
|
| 459 |
lf[idx] /= penalty
|
| 460 |
-
|
| 461 |
if top_k > 0:
|
| 462 |
tv, _ = torch.topk(lf, min(top_k, lf.size(-1)))
|
| 463 |
lf[lf < tv[-1]] = float("-inf")
|
|
@@ -468,28 +394,19 @@ class LinnyModel:
|
|
| 468 |
rm[..., 1:] = rm[..., :-1].clone()
|
| 469 |
rm[..., 0] = False
|
| 470 |
lf[si[rm]] = float("-inf")
|
| 471 |
-
|
| 472 |
idx = torch.multinomial(torch.softmax(lf, dim=-1), 1).item()
|
| 473 |
char = self.itos[idx]
|
| 474 |
-
token_count += 1
|
| 475 |
-
|
| 476 |
-
# Update recent tokens (store char for penalty)
|
| 477 |
recent_tokens.append(char)
|
| 478 |
-
|
| 479 |
-
|
| 480 |
if char == "#" and generated.endswith("##"):
|
| 481 |
break
|
| 482 |
-
|
| 483 |
generated += char
|
| 484 |
yield char
|
| 485 |
-
|
| 486 |
-
# Signal hit limit for char mode too
|
| 487 |
-
if token_count >= max_len - 5:
|
| 488 |
-
yield "__HIT_LIMIT__"
|
| 489 |
-
|
| 490 |
|
| 491 |
# ───────────────��─────────────────────────
|
| 492 |
-
# 🧰 Helpers (unchanged)
|
| 493 |
# ─────────────────────────────────────────
|
| 494 |
def parse_think_tags(text: str):
|
| 495 |
if "<think>" not in text:
|
|
@@ -500,9 +417,7 @@ def parse_think_tags(text: str):
|
|
| 500 |
return inner.strip(), (before + after).strip(), True
|
| 501 |
return rest.strip(), before.strip(), False
|
| 502 |
|
| 503 |
-
|
| 504 |
def extract_current_topic(thinking_text: str) -> str:
|
| 505 |
-
"""Grab the most recent **Topic** marker from thinking text."""
|
| 506 |
if not thinking_text:
|
| 507 |
return "Reasoning..."
|
| 508 |
matches = re.findall(r'\*\*([^*]+)\*\*', thinking_text)
|
|
@@ -510,51 +425,41 @@ def extract_current_topic(thinking_text: str) -> str:
|
|
| 510 |
return f"Reasoning: {matches[-1].strip()}"
|
| 511 |
return "Reasoning..."
|
| 512 |
|
| 513 |
-
|
| 514 |
def format_message(visible: str, thinking: str | None,
|
| 515 |
think_complete: bool, think_elapsed: float | None,
|
| 516 |
current_topic: str = "Reasoning...") -> str:
|
| 517 |
if not thinking:
|
| 518 |
return visible
|
| 519 |
-
|
| 520 |
if think_complete and think_elapsed is not None:
|
| 521 |
-
summary
|
| 522 |
-
open_attr = ""
|
| 523 |
else:
|
| 524 |
-
summary
|
| 525 |
-
open_attr = ""
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
f"<div class='think-content'>{thinking}</div>"
|
| 531 |
-
f"</details>"
|
| 532 |
-
)
|
| 533 |
if visible.strip():
|
| 534 |
return think_block + "\n\n" + visible
|
| 535 |
return think_block
|
| 536 |
|
| 537 |
-
|
| 538 |
def extract_html_canvas(text: str):
|
| 539 |
pattern = r"```html\s*\n([\s\S]*?)```"
|
| 540 |
-
match
|
| 541 |
if match:
|
| 542 |
-
html_code
|
| 543 |
cleaned_text = text[:match.start()] + text[match.end():]
|
| 544 |
return html_code.strip(), cleaned_text.strip()
|
| 545 |
return None, text
|
| 546 |
|
| 547 |
-
|
| 548 |
def make_canvas_html(code: str) -> str:
|
| 549 |
escaped = code.replace('"', """)
|
| 550 |
-
return (
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
"</div>"
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
|
| 559 |
# ─────────────────────────────────────────
|
| 560 |
# 🚀 Auto-load
|
|
@@ -565,24 +470,18 @@ _startup_msg = "⚠️ No model found. Place your `.pt` in the Space root."
|
|
| 565 |
|
| 566 |
if MODEL_PATH.exists():
|
| 567 |
try:
|
| 568 |
-
_startup_cfg
|
| 569 |
-
_startup_model = LinnyModel(MODEL_PATH, _startup_cfg,
|
| 570 |
-
|
| 571 |
-
wm = _startup_cfg.get("working_memory", 0)
|
| 572 |
mtype = _startup_cfg.get("model_type", "char")
|
| 573 |
epoch = _startup_model.epoch
|
| 574 |
-
_startup_msg = (
|
| 575 |
-
|
| 576 |
-
f"({_startup_cfg['hidden_layers']}L × {_startup_cfg['neurons']}N, "
|
| 577 |
-
f"epoch {epoch}, {mtype}"
|
| 578 |
-
+ (f", {wm} ctx)" if wm else ")")
|
| 579 |
-
)
|
| 580 |
print(_startup_msg)
|
| 581 |
except Exception as e:
|
| 582 |
_startup_msg = f"❌ Auto-load failed: {e}"
|
| 583 |
print(_startup_msg)
|
| 584 |
|
| 585 |
-
|
| 586 |
# ─────────────────────────────────────────
|
| 587 |
# 🎨 CSS (unchanged)
|
| 588 |
# ─────────────────────────────────────────
|
|
@@ -610,15 +509,12 @@ details[open] .think-summary::before { content: "▼ "; }
|
|
| 610 |
.tab-nav { background: #0f0f1a !important; border-bottom: 1px solid #2d2d4e !important; }
|
| 611 |
"""
|
| 612 |
|
| 613 |
-
|
| 614 |
# ─────────────────────────────────────────
|
| 615 |
-
# 🖥️ UI (
|
| 616 |
# ─────────────────────────────────────────
|
| 617 |
def build_ui():
|
| 618 |
cfg = load_config()
|
| 619 |
-
|
| 620 |
with gr.Blocks(title="Linny AI", css=CSS) as demo:
|
| 621 |
-
|
| 622 |
session_model = gr.State(_startup_model)
|
| 623 |
session_cfg = gr.State(_startup_cfg)
|
| 624 |
|
|
@@ -630,168 +526,97 @@ def build_ui():
|
|
| 630 |
""")
|
| 631 |
|
| 632 |
with gr.Tabs():
|
| 633 |
-
|
| 634 |
-
# ══════════════════════════════════════════
|
| 635 |
-
# TAB 1 — Chat
|
| 636 |
-
# ══════════════════════════════════════════
|
| 637 |
with gr.TabItem("💬 Chat"):
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
value=_startup_msg,
|
| 641 |
-
elem_classes=["status-bar"],
|
| 642 |
-
)
|
| 643 |
-
|
| 644 |
-
chatbot = gr.Chatbot(
|
| 645 |
-
elem_id="chatbox",
|
| 646 |
-
label="",
|
| 647 |
-
render_markdown=True,
|
| 648 |
-
)
|
| 649 |
-
|
| 650 |
canvas_display = gr.HTML(visible=False)
|
| 651 |
|
| 652 |
with gr.Row(elem_classes=["input-row"]):
|
| 653 |
-
msg_box
|
| 654 |
-
placeholder="Message Linny…",
|
| 655 |
-
show_label=False,
|
| 656 |
-
scale=8,
|
| 657 |
-
lines=1,
|
| 658 |
-
autofocus=True,
|
| 659 |
-
)
|
| 660 |
send_btn = gr.Button("Send ↩", variant="primary", scale=1)
|
| 661 |
|
| 662 |
with gr.Row(elem_classes=["reasoning-row"]):
|
| 663 |
-
reasoning_toggle = gr.Checkbox(
|
| 664 |
-
value=False,
|
| 665 |
-
label="🧠 Force Reasoning (pre-fills <think> token)",
|
| 666 |
-
scale=1,
|
| 667 |
-
)
|
| 668 |
|
| 669 |
with gr.Accordion("⚙️ Generation Settings", open=False):
|
| 670 |
with gr.Row():
|
| 671 |
-
temp_sl
|
| 672 |
-
penalty_sl = gr.Slider(1.0, 2.0, value=cfg.get("default_penalty", 1.
|
| 673 |
with gr.Row():
|
| 674 |
-
penalty_window_sl = gr.Slider(10, 500, value=cfg.get("default_penalty_window", 110), step=10, label="Penalty Window
|
| 675 |
with gr.Row():
|
| 676 |
-
topp_sl
|
| 677 |
-
topk_sl
|
| 678 |
-
max_len_sl = gr.Slider(100, 8000, value=cfg.get("default_max_len",
|
| 679 |
-
label="Max Response Length (auto-capped by Working Memory)")
|
| 680 |
|
| 681 |
with gr.Accordion("📤 Upload Your Own Model (optional)", open=False):
|
| 682 |
gr.Markdown("Upload a `.pt` file and optionally a tokenizer `.json` for token-based models.")
|
| 683 |
with gr.Row():
|
| 684 |
-
user_pt
|
| 685 |
user_tok = gr.File(label="Upload tokenizer .json (token model only)", file_types=[".json"])
|
| 686 |
-
|
| 687 |
-
model_type_radio = gr.Radio(
|
| 688 |
-
choices=["char", "token"], value="char",
|
| 689 |
-
label="Model Type",
|
| 690 |
-
)
|
| 691 |
-
|
| 692 |
with gr.Row():
|
| 693 |
-
u_layers
|
| 694 |
-
u_neurons = gr.Number(value=768, label="Neurons",
|
| 695 |
-
u_embed
|
| 696 |
-
u_dropout = gr.Number(value=0.2, label="Dropout",
|
| 697 |
u_working_mem = gr.Number(value=2048, label="Working Memory (0=off)", precision=0)
|
| 698 |
with gr.Row():
|
| 699 |
u_user_tag = gr.Textbox(value="### Instruction:", label="User Tag")
|
| 700 |
-
u_bot_tag
|
| 701 |
-
u_eos
|
| 702 |
-
|
| 703 |
-
load_user_btn = gr.Button("🚀 Load My Model", variant="secondary")
|
| 704 |
user_load_status = gr.Markdown("")
|
| 705 |
|
| 706 |
-
# ══════════════════════════════════════════
|
| 707 |
-
# TAB 2 — Admin (UPDATED with new settings)
|
| 708 |
-
# ══════════════════════════════════════════
|
| 709 |
with gr.TabItem("🔒 Admin"):
|
| 710 |
-
gr.Markdown(
|
| 711 |
-
"## Admin Panel\n"
|
| 712 |
-
"Edit `config.json` settings. Upload `default_model.pt` via HF Files tab."
|
| 713 |
-
)
|
| 714 |
-
|
| 715 |
with gr.Row():
|
| 716 |
-
admin_pw
|
| 717 |
-
placeholder="Enter password", scale=3)
|
| 718 |
admin_login_btn = gr.Button("Login", scale=1)
|
| 719 |
admin_status = gr.Markdown("")
|
| 720 |
-
|
| 721 |
with gr.Group(visible=False) as admin_panel:
|
| 722 |
gr.Markdown("### Model Architecture")
|
| 723 |
-
model_type_admin = gr.Radio(
|
| 724 |
-
choices=["char", "token"],
|
| 725 |
-
value=cfg.get("model_type", "char"),
|
| 726 |
-
label="Model Type",
|
| 727 |
-
)
|
| 728 |
with gr.Row():
|
| 729 |
-
a_layers
|
| 730 |
-
a_neurons = gr.Number(value=cfg.get("neurons", 768),
|
| 731 |
-
a_embed
|
| 732 |
-
a_dropout = gr.Number(value=cfg.get("dropout", 0.2),
|
| 733 |
-
a_working_mem = gr.Number(value=cfg.get("working_memory", 2048),
|
| 734 |
-
label="Working Memory (0=off)", precision=0)
|
| 735 |
|
| 736 |
gr.Markdown("### Tags & Tokens")
|
| 737 |
with gr.Row():
|
| 738 |
-
a_user_tag = gr.Textbox(value=cfg.get("user_tag",
|
| 739 |
-
a_bot_tag
|
| 740 |
-
a_eos
|
| 741 |
-
a_sys = gr.Textbox(
|
| 742 |
-
value=cfg.get("system_prompt", "You are a helpful AI named Linny."),
|
| 743 |
-
label="System Prompt", lines=2,
|
| 744 |
-
)
|
| 745 |
|
| 746 |
gr.Markdown("### Reasoning")
|
| 747 |
-
a_reasoning_mode = gr.Radio(
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
info="prompt_suffix = appends /think to prompt | response_prefix = prefills <think> token",
|
| 752 |
-
)
|
| 753 |
-
a_reasoning_start = gr.Checkbox(
|
| 754 |
-
value=cfg.get("reasoning_start", True),
|
| 755 |
-
label="Enable Reasoning Start Prefix",
|
| 756 |
-
info="Pre-fills 'I need to think about this...' after <think>",
|
| 757 |
-
)
|
| 758 |
-
# ADDED: New reasoning limits
|
| 759 |
-
a_min_response_tokens = gr.Number(
|
| 760 |
-
value=cfg.get("min_response_tokens", 3),
|
| 761 |
-
label="Minimum Response Tokens After </think>",
|
| 762 |
-
precision=0
|
| 763 |
-
)
|
| 764 |
-
a_max_reasoning_tokens = gr.Number(
|
| 765 |
-
value=cfg.get("max_reasoning_tokens", 2500),
|
| 766 |
-
label="Max Reasoning Tokens (auto-close if exceeded)",
|
| 767 |
-
precision=0
|
| 768 |
-
)
|
| 769 |
|
| 770 |
gr.Markdown("### Default Generation Settings")
|
| 771 |
with gr.Row():
|
| 772 |
-
a_temp
|
| 773 |
-
a_penalty = gr.Slider(1.0, 2.0, value=cfg.get("default_penalty", 1.
|
| 774 |
with gr.Row():
|
| 775 |
a_penalty_window = gr.Slider(10, 500, value=cfg.get("default_penalty_window", 110), step=10, label="Default Penalty Window")
|
| 776 |
with gr.Row():
|
| 777 |
-
a_topp
|
| 778 |
-
a_topk
|
| 779 |
-
a_maxlen = gr.Slider(100, 8000, value=cfg.get("default_max_len",
|
| 780 |
|
| 781 |
save_cfg_btn = gr.Button("💾 Save config.json", variant="primary")
|
| 782 |
-
save_status
|
| 783 |
-
|
| 784 |
-
# ════════════════════════════════════════════════
|
| 785 |
-
# Callbacks (UPDATED with new fields)
|
| 786 |
-
# ════════════════════════════════════════════════
|
| 787 |
|
|
|
|
| 788 |
def do_admin_login(pw):
|
| 789 |
if pw == ADMIN_PASSWORD:
|
| 790 |
return gr.update(visible=True), "✅ Logged in."
|
| 791 |
return gr.update(visible=False), "❌ Incorrect password."
|
| 792 |
-
|
| 793 |
-
admin_login_btn.click(do_admin_login, inputs=[admin_pw],
|
| 794 |
-
outputs=[admin_panel, admin_status])
|
| 795 |
|
| 796 |
def do_save_config(mtype, layers, neurons, embed, dropout, working_mem,
|
| 797 |
user_tag, bot_tag, eos, sys_prompt,
|
|
@@ -800,94 +625,83 @@ def build_ui():
|
|
| 800 |
temp, penalty, penalty_window, top_p, top_k, max_len):
|
| 801 |
try:
|
| 802 |
new_cfg = {
|
| 803 |
-
"model_type":
|
| 804 |
-
"hidden_layers":
|
| 805 |
-
"neurons":
|
| 806 |
-
"embed_size":
|
| 807 |
-
"dropout":
|
| 808 |
-
"working_memory":
|
| 809 |
-
"user_tag":
|
| 810 |
-
"bot_tag":
|
| 811 |
-
"eos_token":
|
| 812 |
-
"system_prompt":
|
| 813 |
-
"reasoning_mode":
|
| 814 |
"reasoning_start": bool(reasoning_start),
|
| 815 |
"min_response_tokens": int(min_response_tokens),
|
| 816 |
"max_reasoning_tokens": int(max_reasoning_tokens),
|
| 817 |
-
"default_temp":
|
| 818 |
"default_penalty": float(penalty),
|
| 819 |
"default_penalty_window": int(penalty_window),
|
| 820 |
-
"default_top_p":
|
| 821 |
-
"default_top_k":
|
| 822 |
"default_max_len": int(max_len),
|
| 823 |
}
|
| 824 |
save_config(new_cfg)
|
| 825 |
-
return "✅ config.json saved! Restart the Space to apply
|
| 826 |
except Exception as e:
|
| 827 |
return f"❌ Error: {e}"
|
| 828 |
-
|
| 829 |
-
save_cfg_btn.click(
|
| 830 |
-
do_save_config,
|
| 831 |
inputs=[model_type_admin, a_layers, a_neurons, a_embed, a_dropout, a_working_mem,
|
| 832 |
a_user_tag, a_bot_tag, a_eos, a_sys,
|
| 833 |
a_reasoning_mode, a_reasoning_start,
|
| 834 |
a_min_response_tokens, a_max_reasoning_tokens,
|
| 835 |
a_temp, a_penalty, a_penalty_window, a_topp, a_topk, a_maxlen],
|
| 836 |
-
outputs=[save_status]
|
| 837 |
-
)
|
| 838 |
|
| 839 |
def load_user_model(pt_file, tok_file, mtype, layers, neurons, embed, dropout,
|
| 840 |
-
|
| 841 |
if pt_file is None:
|
| 842 |
return None, None, "❌ Please upload a .pt file first."
|
| 843 |
try:
|
| 844 |
user_cfg = {
|
| 845 |
-
"model_type":
|
| 846 |
-
"hidden_layers":
|
| 847 |
-
"neurons":
|
| 848 |
-
"embed_size":
|
| 849 |
-
"dropout":
|
| 850 |
"working_memory": int(working_mem),
|
| 851 |
-
"user_tag":
|
| 852 |
-
"bot_tag":
|
| 853 |
-
"eos_token":
|
| 854 |
-
"reasoning_mode":
|
| 855 |
-
"reasoning_start": cfg.get("reasoning_start",
|
| 856 |
"min_response_tokens": cfg.get("min_response_tokens", 3),
|
| 857 |
"max_reasoning_tokens": cfg.get("max_reasoning_tokens", 2500),
|
| 858 |
}
|
| 859 |
tok_path = tok_file.name if tok_file else None
|
| 860 |
-
m
|
| 861 |
-
|
| 862 |
-
return m, user_cfg, f"✅ Your model loaded! ({user_cfg['hidden_layers']}L × {user_cfg['neurons']}N, {mtype}, {wm} ctx, epoch {m.epoch})"
|
| 863 |
except Exception as e:
|
| 864 |
return None, None, f"❌ Error: {e}"
|
| 865 |
-
|
| 866 |
-
load_user_btn.click(
|
| 867 |
-
load_user_model,
|
| 868 |
inputs=[user_pt, user_tok, model_type_radio,
|
| 869 |
u_layers, u_neurons, u_embed, u_dropout, u_working_mem,
|
| 870 |
u_user_tag, u_bot_tag, u_eos],
|
| 871 |
-
outputs=[session_model, session_cfg, user_load_status]
|
| 872 |
-
)
|
| 873 |
|
| 874 |
def respond(message, history, model, temp, penalty, penalty_window, top_p, top_k, max_len, force_thinking):
|
| 875 |
if not message.strip():
|
| 876 |
yield history, "", gr.update(visible=False)
|
| 877 |
return
|
| 878 |
-
|
| 879 |
if model is None:
|
| 880 |
-
yield history + [{"role":"user","content":message},{"role":"assistant","content":"⚠️ No model loaded.
|
| 881 |
return
|
| 882 |
|
| 883 |
history = history + [{"role":"user","content":message},{"role":"assistant","content":""}]
|
| 884 |
-
|
| 885 |
-
eos = model.config.get("eos_token", "<|end|>")
|
| 886 |
full_response = ""
|
| 887 |
-
stop
|
| 888 |
-
think_start
|
| 889 |
-
think_end
|
| 890 |
-
hit_limit = False # ADDED: track if we hit token limit
|
| 891 |
|
| 892 |
try:
|
| 893 |
for chunk in model.stream_generate(
|
|
@@ -895,58 +709,36 @@ def build_ui():
|
|
| 895 |
temperature=float(temp),
|
| 896 |
max_len=int(max_len),
|
| 897 |
penalty=float(penalty),
|
| 898 |
-
penalty_window=int(penalty_window),
|
| 899 |
top_p=float(top_p),
|
| 900 |
top_k=int(top_k),
|
| 901 |
force_thinking=bool(force_thinking),
|
| 902 |
-
min_response_tokens=model.config.get("min_response_tokens", 3),
|
| 903 |
-
max_reasoning_tokens=model.config.get("max_reasoning_tokens", 2500),
|
| 904 |
):
|
| 905 |
-
# Check for hit limit marker
|
| 906 |
-
if chunk == "__HIT_LIMIT__":
|
| 907 |
-
hit_limit = True
|
| 908 |
-
continue
|
| 909 |
-
|
| 910 |
full_response += chunk
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
| 914 |
if full_response.count("<think>") > full_response.count("</think>"):
|
| 915 |
full_response += "</think>"
|
| 916 |
else:
|
| 917 |
stop = True
|
| 918 |
|
| 919 |
thinking, visible, think_complete = parse_think_tags(full_response)
|
| 920 |
-
|
| 921 |
-
# Timer: start on first thinking char, STOP when </think> closes
|
| 922 |
if thinking and think_start is None:
|
| 923 |
think_start = time.time()
|
| 924 |
if think_complete and think_end is None and think_start is not None:
|
| 925 |
think_end = time.time()
|
| 926 |
-
|
| 927 |
-
if think_end is not None:
|
| 928 |
-
think_elapsed = think_end - think_start
|
| 929 |
-
elif think_start is not None:
|
| 930 |
-
think_elapsed = time.time() - think_start
|
| 931 |
-
else:
|
| 932 |
-
think_elapsed = None
|
| 933 |
-
|
| 934 |
current_topic = extract_current_topic(thinking) if thinking else "Reasoning..."
|
| 935 |
-
|
| 936 |
html_code, visible_clean = extract_html_canvas(visible)
|
| 937 |
-
canvas_update = gr.update(
|
| 938 |
-
|
| 939 |
-
value=make_canvas_html(html_code) if html_code else "",
|
| 940 |
-
)
|
| 941 |
-
|
| 942 |
-
history[-1] = {"role":"assistant","content":format_message(
|
| 943 |
-
visible_clean, thinking, think_complete, think_elapsed, current_topic
|
| 944 |
-
)}
|
| 945 |
yield history, "", canvas_update
|
| 946 |
-
|
| 947 |
if stop:
|
| 948 |
break
|
| 949 |
-
|
| 950 |
except Exception as e:
|
| 951 |
history[-1] = {"role":"assistant","content":f"⚠️ Generation error: {e}"}
|
| 952 |
yield history, "", gr.update(visible=False)
|
|
@@ -957,35 +749,21 @@ def build_ui():
|
|
| 957 |
if think_end is None and think_start is not None:
|
| 958 |
think_end = time.time()
|
| 959 |
think_elapsed = (think_end - think_start) if (think_end and think_start) else None
|
| 960 |
-
|
| 961 |
if think_complete and not visible.strip():
|
| 962 |
visible = "*(no response generated)*"
|
| 963 |
-
|
| 964 |
-
# ADDED: Add note if hit token limit
|
| 965 |
-
if hit_limit:
|
| 966 |
-
visible += "\n\n---\n⚠️ *Response reached token limit. You can continue generating if this feature is implemented in UI.*"
|
| 967 |
-
|
| 968 |
current_topic = extract_current_topic(thinking) if thinking else "Reasoning..."
|
| 969 |
html_code, visible_clean = extract_html_canvas(visible)
|
| 970 |
-
history[-1] = {"role":"assistant","content":format_message(
|
| 971 |
-
|
| 972 |
-
think_elapsed=think_elapsed, current_topic=current_topic
|
| 973 |
-
)}
|
| 974 |
-
canvas_update = gr.update(
|
| 975 |
-
visible=bool(html_code),
|
| 976 |
-
value=make_canvas_html(html_code) if html_code else "",
|
| 977 |
-
)
|
| 978 |
yield history, "", canvas_update
|
| 979 |
|
| 980 |
-
shared_inputs
|
| 981 |
-
|
| 982 |
shared_outputs = [chatbot, msg_box, canvas_display]
|
| 983 |
-
|
| 984 |
-
send_btn.click(respond, inputs=shared_inputs, outputs=shared_outputs)
|
| 985 |
msg_box.submit(respond, inputs=shared_inputs, outputs=shared_outputs)
|
| 986 |
|
| 987 |
return demo
|
| 988 |
|
| 989 |
-
|
| 990 |
if __name__ == "__main__":
|
| 991 |
build_ui().launch(ssr_mode=False)
|
|
|
|
| 2 |
import re
|
| 3 |
import json
|
| 4 |
import time
|
|
|
|
| 5 |
import torch
|
| 6 |
import torch.nn as nn
|
| 7 |
import gradio as gr
|
| 8 |
from pathlib import Path
|
| 9 |
+
from collections import deque
|
| 10 |
|
| 11 |
# ─────────────────────────────────────────
|
| 12 |
# 🔐 Admin password
|
|
|
|
| 22 |
_pt_files = sorted(SPACE_ROOT.glob("*.pt"))
|
| 23 |
MODEL_PATH = _pt_files[0] if _pt_files else SPACE_ROOT / "default_model.pt"
|
| 24 |
|
|
|
|
| 25 |
_tok_files = [f for f in SPACE_ROOT.glob("*.json") if f.name != "config.json"]
|
| 26 |
TOKENIZER_PATH = _tok_files[0] if _tok_files else None
|
| 27 |
|
|
|
|
| 29 |
# 🗃️ Config
|
| 30 |
# ─────────────────────────────────────────
|
| 31 |
DEFAULT_CONFIG = {
|
| 32 |
+
"model_type": "char",
|
| 33 |
"hidden_layers": 5,
|
| 34 |
"neurons": 768,
|
| 35 |
"embed_size": 384,
|
|
|
|
| 40 |
"bot_tag": "### Response:",
|
| 41 |
"eos_token": "<|end|>",
|
| 42 |
"system_prompt": "You are a helpful and intelligent AI assistant named Linny.",
|
| 43 |
+
"default_temp": 0.8,
|
| 44 |
+
"default_penalty": 1.05,
|
| 45 |
+
"default_penalty_window": 110,
|
| 46 |
+
"default_top_p": 0.4,
|
| 47 |
+
"default_top_k": 65,
|
| 48 |
+
"default_max_len": 2600,
|
| 49 |
+
"reasoning_mode": "response_prefix",
|
| 50 |
+
"reasoning_start": False,
|
| 51 |
+
"min_response_tokens": 3,
|
| 52 |
+
"max_reasoning_tokens": 2500,
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
def load_config() -> dict:
|
|
|
|
| 65 |
with open(CONFIG_PATH, "w") as f:
|
| 66 |
json.dump(cfg, f, indent=2)
|
| 67 |
|
|
|
|
| 68 |
# ─────────────────────────────────────────
|
| 69 |
# 🧠 Model Architectures (unchanged)
|
| 70 |
# ─────────────────────────────────────────
|
|
|
|
| 73 |
super().__init__()
|
| 74 |
self.embed = nn.Embedding(vocab_size, embed_size)
|
| 75 |
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers=num_layers,
|
| 76 |
+
batch_first=True, dropout=dropout if num_layers > 1 else 0)
|
| 77 |
+
self.fc = nn.Linear(hidden_size, vocab_size)
|
|
|
|
|
|
|
| 78 |
def forward(self, x, hidden=None):
|
| 79 |
+
out, hidden = self.lstm(self.embed(x), hidden)
|
|
|
|
| 80 |
return self.fc(out), hidden
|
| 81 |
|
|
|
|
| 82 |
class LSTMTokenLM(nn.Module):
|
| 83 |
def __init__(self, vocab_size, embed_size, hidden_size, num_layers, dropout=0.2):
|
| 84 |
super().__init__()
|
| 85 |
self.embed = nn.Embedding(vocab_size, embed_size)
|
| 86 |
self.lstm = nn.LSTM(embed_size, hidden_size, num_layers=num_layers,
|
| 87 |
+
batch_first=True, dropout=dropout if num_layers > 1 else 0)
|
| 88 |
+
self.fc = nn.Linear(hidden_size, vocab_size)
|
|
|
|
|
|
|
| 89 |
def forward(self, x, hidden=None):
|
| 90 |
out, hidden = self.lstm(self.embed(x), hidden)
|
| 91 |
return self.fc(out), hidden
|
| 92 |
|
|
|
|
| 93 |
# ─────────────────────────────────────────
|
| 94 |
+
# 🔤 GPT-2 byte decoder
|
| 95 |
# ─────────────────────────────────────────
|
| 96 |
def _build_byte_decoder():
|
| 97 |
bs = (list(range(ord('!'), ord('~')+1)) +
|
|
|
|
| 102 |
for b in range(256):
|
| 103 |
if b not in bs:
|
| 104 |
bs.append(b)
|
| 105 |
+
cs.append(256+n)
|
| 106 |
n += 1
|
| 107 |
return {chr(c): b for b, c in zip(bs, cs)}
|
| 108 |
|
|
|
|
| 114 |
except KeyError:
|
| 115 |
return tok_str.encode('utf-8', errors='replace')
|
| 116 |
|
|
|
|
| 117 |
# ─────────────────────────────────────────
|
| 118 |
+
# ⚙️ Model Loader (identical generation to local)
|
| 119 |
# ─────────────────────────────────────────
|
| 120 |
class LinnyModel:
|
| 121 |
def __init__(self, pt_path, config: dict, tokenizer_path=None):
|
| 122 |
self.config = config
|
| 123 |
self.model_type = config.get("model_type", "char")
|
| 124 |
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
ckpt = torch.load(pt_path, map_location=self.device, weights_only=False)
|
| 127 |
|
| 128 |
if self.model_type == "token":
|
| 129 |
+
from tokenizers import Tokenizer as HFTokenizer
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
tok_path = tokenizer_path or config.get("tokenizer_path")
|
| 131 |
if not tok_path or not Path(str(tok_path)).exists():
|
| 132 |
raise FileNotFoundError(f"Tokenizer not found: {tok_path}")
|
|
|
|
| 133 |
self.tokenizer = HFTokenizer.from_file(str(tok_path))
|
| 134 |
+
vocab_size = self.tokenizer.get_vocab_size()
|
| 135 |
+
self.chars = None; self.stoi = None; self.itos = None
|
| 136 |
+
arch = ckpt.get('config', {})
|
|
|
|
|
|
|
|
|
|
| 137 |
layers = arch.get('hidden_layers', config['hidden_layers'])
|
| 138 |
neurons = arch.get('neurons', config['neurons'])
|
| 139 |
embed = arch.get('embed_size', config['embed_size'])
|
| 140 |
dropout = arch.get('dropout', config.get('dropout', 0.2))
|
|
|
|
| 141 |
self.model = LSTMTokenLM(vocab_size, embed, neurons, layers, dropout).to(self.device)
|
| 142 |
self.model.load_state_dict(ckpt['model_state'])
|
|
|
|
| 143 |
else:
|
| 144 |
self.tokenizer = None
|
| 145 |
+
self.chars = ckpt["chars"]
|
| 146 |
+
self.stoi = {ch: i for i, ch in enumerate(self.chars)}
|
| 147 |
+
self.itos = {i: ch for i, ch in enumerate(self.chars)}
|
|
|
|
| 148 |
dropout = ckpt.get("config", {}).get("dropout", config.get("dropout", 0.2))
|
| 149 |
+
self.model = LSTMCharLM(len(self.chars), config["embed_size"],
|
| 150 |
+
config["neurons"], config["hidden_layers"], dropout).to(self.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
self.model.load_state_dict(ckpt["model_state"])
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
self.model.eval()
|
| 154 |
self.epoch = ckpt.get('epoch', '?')
|
| 155 |
|
| 156 |
+
# ------------------------------------------------------------------
|
| 157 |
+
# Exact copy of local server's generate_stream logic
|
| 158 |
+
# ------------------------------------------------------------------
|
| 159 |
+
def stream_generate(self, prompt, temperature=0.8, max_len=2600,
|
| 160 |
+
penalty=1.05, penalty_window=110,
|
| 161 |
+
top_p=0.4, top_k=65, force_thinking=False,
|
| 162 |
+
prefix_text="", penalize_prefix=False,
|
| 163 |
+
min_response_tokens=3, max_reasoning_tokens=2500):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
"""
|
| 165 |
+
Matches local linny_server.py generate_stream exactly.
|
| 166 |
"""
|
| 167 |
+
cfg = self.config
|
| 168 |
user_tag = cfg.get("user_tag", "### Instruction:")
|
| 169 |
+
bot_tag = cfg.get("bot_tag", "### Response:")
|
| 170 |
+
r_mode = cfg.get("reasoning_mode", "response_prefix")
|
| 171 |
eos_token_str = cfg.get("eos_token", "<|end|>")
|
| 172 |
|
| 173 |
# Apply prompt_suffix mode
|
|
|
|
| 177 |
actual_prompt = prompt.strip() + " /think"
|
| 178 |
|
| 179 |
formatted = f"{user_tag}\n{actual_prompt}\n\n{bot_tag}\n"
|
| 180 |
+
|
| 181 |
+
# Working memory cap
|
| 182 |
working_memory = cfg.get("working_memory", 0)
|
| 183 |
if working_memory > 0:
|
| 184 |
max_len = max(50, min(max_len, working_memory - len(formatted)))
|
| 185 |
|
| 186 |
hidden = None
|
| 187 |
generated = ""
|
|
|
|
|
|
|
| 188 |
recent_tokens = deque(maxlen=penalty_window)
|
| 189 |
+
|
| 190 |
+
# State tracking (exactly as local)
|
| 191 |
in_reasoning = False
|
| 192 |
think_closed = False
|
| 193 |
awaiting_response = False
|
| 194 |
response_token_count = 0
|
| 195 |
reasoning_toks = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
with torch.no_grad():
|
| 198 |
# Encode the conversation prefix
|
|
|
|
| 201 |
else:
|
| 202 |
ids = [self.stoi.get(c, 0) for c in formatted]
|
| 203 |
|
| 204 |
+
t = torch.tensor([ids], dtype=torch.long, device=self.device)
|
| 205 |
_, hidden = self.model(t, hidden)
|
| 206 |
+
input_token = torch.tensor([[ids[-1]]], dtype=torch.long, device=self.device)
|
| 207 |
+
|
| 208 |
+
# If we have existing assistant response (continue mode)
|
| 209 |
if prefix_text:
|
| 210 |
if self.model_type == "token":
|
| 211 |
prefix_ids = self.tokenizer.encode(prefix_text).ids
|
| 212 |
else:
|
| 213 |
prefix_ids = [self.stoi.get(c, 0) for c in prefix_text]
|
|
|
|
| 214 |
if prefix_ids:
|
| 215 |
+
pt = torch.tensor([prefix_ids], dtype=torch.long, device=self.device)
|
| 216 |
_, hidden = self.model(pt, hidden)
|
| 217 |
generated = prefix_text
|
| 218 |
+
input_token = torch.tensor([[prefix_ids[-1]]], dtype=torch.long, device=self.device)
|
| 219 |
if penalize_prefix:
|
| 220 |
recent_tokens.extend(prefix_ids)
|
|
|
|
| 221 |
# Update state based on prefix
|
| 222 |
if "<think>" in prefix_text and "</think>" not in prefix_text:
|
| 223 |
in_reasoning = True
|
|
|
|
| 224 |
elif "</think>" in prefix_text:
|
| 225 |
think_closed = True
|
| 226 |
awaiting_response = True
|
| 227 |
|
| 228 |
+
# Prefill <think> if in response_prefix mode and no prefix
|
| 229 |
if not prefix_text and force_thinking and r_mode == "response_prefix":
|
| 230 |
+
if self.model_type == "token":
|
| 231 |
+
think_id = self.tokenizer.token_to_id("<think>")
|
| 232 |
+
if think_id is not None:
|
| 233 |
+
tt = torch.tensor([[think_id]], dtype=torch.long, device=self.device)
|
| 234 |
+
_, hidden = self.model(tt, hidden)
|
| 235 |
+
input_token = tt
|
| 236 |
+
generated = "<think>"
|
| 237 |
+
yield "<think>"
|
| 238 |
+
in_reasoning = True
|
| 239 |
+
else:
|
| 240 |
+
# Char mode: prime hidden with "<think>"
|
| 241 |
+
think_ids = [self.stoi.get(ch, 0) for ch in "<think>"]
|
| 242 |
+
tt = torch.tensor([think_ids], dtype=torch.long, device=self.device)
|
| 243 |
_, hidden = self.model(tt, hidden)
|
| 244 |
+
input_token = torch.tensor([[think_ids[-1]]], dtype=torch.long, device=self.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
generated = "<think>"
|
| 246 |
for ch in "<think>":
|
| 247 |
yield ch
|
|
|
|
| 250 |
if cfg.get("reasoning_start", False) and not prefix_text:
|
| 251 |
prefix = f"I need to think about this. The user said '{prompt}'"
|
| 252 |
if self.model_type == "token":
|
| 253 |
+
pids = self.tokenizer.encode(prefix).ids
|
| 254 |
+
pt = torch.tensor([pids], dtype=torch.long, device=self.device)
|
| 255 |
+
_, hidden = self.model(pt, hidden)
|
| 256 |
+
input_token = torch.tensor([[pids[-1]]], dtype=torch.long, device=self.device)
|
| 257 |
generated += prefix
|
| 258 |
yield prefix
|
| 259 |
else:
|
|
|
|
|
|
|
| 260 |
for ch in prefix:
|
| 261 |
+
idx = self.stoi.get(ch, 0)
|
| 262 |
+
it = torch.tensor([[idx]], dtype=torch.long, device=self.device)
|
| 263 |
+
_, hidden = self.model(it, hidden)
|
| 264 |
+
input_token = it
|
| 265 |
+
generated += ch
|
| 266 |
yield ch
|
| 267 |
|
| 268 |
+
# Get special token IDs
|
| 269 |
+
eos_id = None
|
| 270 |
+
think_open_id = None
|
| 271 |
+
think_close_id = None
|
| 272 |
+
if self.model_type == "token":
|
| 273 |
+
eos_id = self.tokenizer.token_to_id(eos_token_str)
|
| 274 |
+
think_open_id = self.tokenizer.token_to_id("<think>")
|
| 275 |
+
think_close_id = self.tokenizer.token_to_id("</think>")
|
| 276 |
+
|
| 277 |
+
# ------------------------------------------------------------------
|
| 278 |
+
# Token generation (exactly as local)
|
| 279 |
+
# ------------------------------------------------------------------
|
| 280 |
if self.model_type == "token":
|
| 281 |
byte_buf = b""
|
|
|
|
|
|
|
| 282 |
for step in range(max_len):
|
| 283 |
logits, hidden = self.model(input_token, hidden)
|
| 284 |
lf = logits[0, -1].float() / max(temperature, 1e-8)
|
| 285 |
|
| 286 |
+
# Repetition penalty
|
| 287 |
if penalty != 1.0 and len(recent_tokens) > 0:
|
| 288 |
penalized_ids = set(recent_tokens)
|
| 289 |
for token_id in penalized_ids:
|
|
|
|
| 304 |
lf[si[rm]] = float("-inf")
|
| 305 |
|
| 306 |
nxt = torch.multinomial(torch.softmax(lf, dim=-1), 1).item()
|
|
|
|
| 307 |
|
| 308 |
# EOS handling with forced minimum response
|
| 309 |
+
if nxt == eos_id:
|
| 310 |
if awaiting_response and response_token_count < min_response_tokens:
|
| 311 |
+
continue
|
| 312 |
else:
|
| 313 |
break
|
| 314 |
|
|
|
|
| 315 |
recent_tokens.append(nxt)
|
| 316 |
|
| 317 |
# Update reasoning state
|
|
|
|
| 323 |
think_closed = True
|
| 324 |
awaiting_response = True
|
| 325 |
response_token_count = 0
|
| 326 |
+
|
| 327 |
if in_reasoning and not think_closed:
|
| 328 |
reasoning_toks += 1
|
| 329 |
+
if max_reasoning_tokens and reasoning_toks >= max_reasoning_tokens:
|
| 330 |
# Force close think
|
| 331 |
if byte_buf:
|
| 332 |
decoded = byte_buf.decode('utf-8', errors='replace')
|
| 333 |
generated += decoded
|
| 334 |
yield decoded
|
| 335 |
byte_buf = b""
|
|
|
|
| 336 |
yield "</think>"
|
| 337 |
in_reasoning = False
|
| 338 |
think_closed = True
|
| 339 |
awaiting_response = True
|
| 340 |
response_token_count = 0
|
| 341 |
+
ct = torch.tensor([[think_close_id]], dtype=torch.long, device=self.device)
|
|
|
|
| 342 |
_, hidden = self.model(ct, hidden)
|
| 343 |
input_token = ct
|
| 344 |
recent_tokens.append(think_close_id)
|
|
|
|
| 349 |
elif not in_reasoning and not think_closed:
|
| 350 |
response_token_count += 1
|
| 351 |
|
| 352 |
+
# Output token
|
| 353 |
tok_str = self.tokenizer.id_to_token(nxt) or ""
|
| 354 |
byte_buf += _tok_to_bytes(tok_str)
|
| 355 |
try:
|
|
|
|
| 359 |
byte_buf = b""
|
| 360 |
except UnicodeDecodeError:
|
| 361 |
pass
|
| 362 |
+
|
| 363 |
+
input_token = torch.tensor([[nxt]], dtype=torch.long, device=self.device)
|
| 364 |
+
|
| 365 |
if byte_buf:
|
| 366 |
leftover = byte_buf.decode('utf-8', errors='replace')
|
| 367 |
generated += leftover
|
| 368 |
yield leftover
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
+
# ------------------------------------------------------------------
|
| 371 |
+
# Character generation (exactly as local)
|
| 372 |
+
# ------------------------------------------------------------------
|
| 373 |
else:
|
|
|
|
| 374 |
for step in range(max_len):
|
| 375 |
logits, hidden = self.model(input_token, hidden)
|
| 376 |
lf = logits[0, -1].float() / max(temperature, 1e-8)
|
| 377 |
+
|
| 378 |
+
# Repetition penalty on characters
|
| 379 |
if penalty != 1.0 and len(recent_tokens) > 0:
|
| 380 |
+
# recent_tokens stores characters (strings)
|
| 381 |
+
penalized_chars = set(recent_tokens)
|
| 382 |
+
for ch in penalized_chars:
|
| 383 |
+
idx = self.stoi.get(ch, 0)
|
|
|
|
|
|
|
| 384 |
if idx < lf.size(0):
|
| 385 |
lf[idx] /= penalty
|
| 386 |
+
|
| 387 |
if top_k > 0:
|
| 388 |
tv, _ = torch.topk(lf, min(top_k, lf.size(-1)))
|
| 389 |
lf[lf < tv[-1]] = float("-inf")
|
|
|
|
| 394 |
rm[..., 1:] = rm[..., :-1].clone()
|
| 395 |
rm[..., 0] = False
|
| 396 |
lf[si[rm]] = float("-inf")
|
| 397 |
+
|
| 398 |
idx = torch.multinomial(torch.softmax(lf, dim=-1), 1).item()
|
| 399 |
char = self.itos[idx]
|
|
|
|
|
|
|
|
|
|
| 400 |
recent_tokens.append(char)
|
| 401 |
+
input_token = torch.tensor([[idx]], dtype=torch.long, device=self.device)
|
| 402 |
+
|
| 403 |
if char == "#" and generated.endswith("##"):
|
| 404 |
break
|
|
|
|
| 405 |
generated += char
|
| 406 |
yield char
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
# ───────────────��─────────────────────────
|
| 409 |
+
# 🧰 Helpers (unchanged from original HF)
|
| 410 |
# ─────────────────────────────────────────
|
| 411 |
def parse_think_tags(text: str):
|
| 412 |
if "<think>" not in text:
|
|
|
|
| 417 |
return inner.strip(), (before + after).strip(), True
|
| 418 |
return rest.strip(), before.strip(), False
|
| 419 |
|
|
|
|
| 420 |
def extract_current_topic(thinking_text: str) -> str:
|
|
|
|
| 421 |
if not thinking_text:
|
| 422 |
return "Reasoning..."
|
| 423 |
matches = re.findall(r'\*\*([^*]+)\*\*', thinking_text)
|
|
|
|
| 425 |
return f"Reasoning: {matches[-1].strip()}"
|
| 426 |
return "Reasoning..."
|
| 427 |
|
|
|
|
| 428 |
def format_message(visible: str, thinking: str | None,
|
| 429 |
think_complete: bool, think_elapsed: float | None,
|
| 430 |
current_topic: str = "Reasoning...") -> str:
|
| 431 |
if not thinking:
|
| 432 |
return visible
|
|
|
|
| 433 |
if think_complete and think_elapsed is not None:
|
| 434 |
+
summary = f"💭 Thought for {think_elapsed:.1f}s"
|
| 435 |
+
open_attr = ""
|
| 436 |
else:
|
| 437 |
+
summary = current_topic
|
| 438 |
+
open_attr = ""
|
| 439 |
+
think_block = (f"<details class='think-details'{open_attr}>"
|
| 440 |
+
f"<summary class='think-summary'>{summary}</summary>"
|
| 441 |
+
f"<div class='think-content'>{thinking}</div>"
|
| 442 |
+
f"</details>")
|
|
|
|
|
|
|
|
|
|
| 443 |
if visible.strip():
|
| 444 |
return think_block + "\n\n" + visible
|
| 445 |
return think_block
|
| 446 |
|
|
|
|
| 447 |
def extract_html_canvas(text: str):
|
| 448 |
pattern = r"```html\s*\n([\s\S]*?)```"
|
| 449 |
+
match = re.search(pattern, text, re.IGNORECASE)
|
| 450 |
if match:
|
| 451 |
+
html_code = match.group(1)
|
| 452 |
cleaned_text = text[:match.start()] + text[match.end():]
|
| 453 |
return html_code.strip(), cleaned_text.strip()
|
| 454 |
return None, text
|
| 455 |
|
|
|
|
| 456 |
def make_canvas_html(code: str) -> str:
|
| 457 |
escaped = code.replace('"', """)
|
| 458 |
+
return (f"<div class='canvas-wrapper'>"
|
| 459 |
+
f"<div class='canvas-label'>🖼️ HTML Canvas</div>"
|
| 460 |
+
f'<iframe class="canvas-frame" srcdoc="{escaped}" '
|
| 461 |
+
f'sandbox="allow-scripts" scrolling="auto"></iframe>'
|
| 462 |
+
f"</div>")
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
# ─────────────────────────────────────────
|
| 465 |
# 🚀 Auto-load
|
|
|
|
| 470 |
|
| 471 |
if MODEL_PATH.exists():
|
| 472 |
try:
|
| 473 |
+
_startup_cfg = load_config()
|
| 474 |
+
_startup_model = LinnyModel(MODEL_PATH, _startup_cfg, tokenizer_path=TOKENIZER_PATH)
|
| 475 |
+
wm = _startup_cfg.get("working_memory", 0)
|
|
|
|
| 476 |
mtype = _startup_cfg.get("model_type", "char")
|
| 477 |
epoch = _startup_model.epoch
|
| 478 |
+
_startup_msg = (f"✅ Model auto-loaded! ({_startup_cfg['hidden_layers']}L × {_startup_cfg['neurons']}N, "
|
| 479 |
+
f"epoch {epoch}, {mtype}" + (f", {wm} ctx)" if wm else ")"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
print(_startup_msg)
|
| 481 |
except Exception as e:
|
| 482 |
_startup_msg = f"❌ Auto-load failed: {e}"
|
| 483 |
print(_startup_msg)
|
| 484 |
|
|
|
|
| 485 |
# ─────────────────────────────────────────
|
| 486 |
# 🎨 CSS (unchanged)
|
| 487 |
# ─────────────────────────────────────────
|
|
|
|
| 509 |
.tab-nav { background: #0f0f1a !important; border-bottom: 1px solid #2d2d4e !important; }
|
| 510 |
"""
|
| 511 |
|
|
|
|
| 512 |
# ─────────────────────────────────────────
|
| 513 |
+
# 🖥️ UI (identical to original HF but with penalty window)
|
| 514 |
# ─────────────────────────────────────────
|
| 515 |
def build_ui():
|
| 516 |
cfg = load_config()
|
|
|
|
| 517 |
with gr.Blocks(title="Linny AI", css=CSS) as demo:
|
|
|
|
| 518 |
session_model = gr.State(_startup_model)
|
| 519 |
session_cfg = gr.State(_startup_cfg)
|
| 520 |
|
|
|
|
| 526 |
""")
|
| 527 |
|
| 528 |
with gr.Tabs():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
with gr.TabItem("💬 Chat"):
|
| 530 |
+
model_status = gr.Markdown(value=_startup_msg, elem_classes=["status-bar"])
|
| 531 |
+
chatbot = gr.Chatbot(elem_id="chatbox", label="", render_markdown=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
canvas_display = gr.HTML(visible=False)
|
| 533 |
|
| 534 |
with gr.Row(elem_classes=["input-row"]):
|
| 535 |
+
msg_box = gr.Textbox(placeholder="Message Linny…", show_label=False, scale=8, lines=1, autofocus=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
send_btn = gr.Button("Send ↩", variant="primary", scale=1)
|
| 537 |
|
| 538 |
with gr.Row(elem_classes=["reasoning-row"]):
|
| 539 |
+
reasoning_toggle = gr.Checkbox(value=False, label="🧠 Force Reasoning (pre-fills <think> token)", scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
with gr.Accordion("⚙️ Generation Settings", open=False):
|
| 542 |
with gr.Row():
|
| 543 |
+
temp_sl = gr.Slider(0.1, 1.5, value=cfg.get("default_temp", 0.8), step=0.05, label="Temperature")
|
| 544 |
+
penalty_sl = gr.Slider(1.0, 2.0, value=cfg.get("default_penalty", 1.05), step=0.05, label="Repetition Penalty")
|
| 545 |
with gr.Row():
|
| 546 |
+
penalty_window_sl = gr.Slider(10, 500, value=cfg.get("default_penalty_window", 110), step=10, label="Penalty Window")
|
| 547 |
with gr.Row():
|
| 548 |
+
topp_sl = gr.Slider(0.0, 1.0, value=cfg.get("default_top_p", 0.4), step=0.05, label="Top-P")
|
| 549 |
+
topk_sl = gr.Slider(0, 100, value=cfg.get("default_top_k", 65), step=1, label="Top-K (0 = off)")
|
| 550 |
+
max_len_sl = gr.Slider(100, 8000, value=cfg.get("default_max_len", 2600), step=50, label="Max Response Length")
|
|
|
|
| 551 |
|
| 552 |
with gr.Accordion("📤 Upload Your Own Model (optional)", open=False):
|
| 553 |
gr.Markdown("Upload a `.pt` file and optionally a tokenizer `.json` for token-based models.")
|
| 554 |
with gr.Row():
|
| 555 |
+
user_pt = gr.File(label="Upload .pt file", file_types=[".pt"])
|
| 556 |
user_tok = gr.File(label="Upload tokenizer .json (token model only)", file_types=[".json"])
|
| 557 |
+
model_type_radio = gr.Radio(choices=["char", "token"], value="char", label="Model Type")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
with gr.Row():
|
| 559 |
+
u_layers = gr.Number(value=5, label="Hidden Layers", precision=0)
|
| 560 |
+
u_neurons = gr.Number(value=768, label="Neurons", precision=0)
|
| 561 |
+
u_embed = gr.Number(value=384, label="Embed Size", precision=0)
|
| 562 |
+
u_dropout = gr.Number(value=0.2, label="Dropout", precision=2)
|
| 563 |
u_working_mem = gr.Number(value=2048, label="Working Memory (0=off)", precision=0)
|
| 564 |
with gr.Row():
|
| 565 |
u_user_tag = gr.Textbox(value="### Instruction:", label="User Tag")
|
| 566 |
+
u_bot_tag = gr.Textbox(value="### Response:", label="Bot Tag")
|
| 567 |
+
u_eos = gr.Textbox(value="<|end|>", label="EOS Token")
|
| 568 |
+
load_user_btn = gr.Button("🚀 Load My Model", variant="secondary")
|
|
|
|
| 569 |
user_load_status = gr.Markdown("")
|
| 570 |
|
|
|
|
|
|
|
|
|
|
| 571 |
with gr.TabItem("🔒 Admin"):
|
| 572 |
+
gr.Markdown("## Admin Panel\nEdit `config.json` settings.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
with gr.Row():
|
| 574 |
+
admin_pw = gr.Textbox(label="Admin Password", type="password", placeholder="Enter password", scale=3)
|
|
|
|
| 575 |
admin_login_btn = gr.Button("Login", scale=1)
|
| 576 |
admin_status = gr.Markdown("")
|
|
|
|
| 577 |
with gr.Group(visible=False) as admin_panel:
|
| 578 |
gr.Markdown("### Model Architecture")
|
| 579 |
+
model_type_admin = gr.Radio(choices=["char", "token"], value=cfg.get("model_type", "char"), label="Model Type")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
with gr.Row():
|
| 581 |
+
a_layers = gr.Number(value=cfg.get("hidden_layers", 5), label="Hidden Layers", precision=0)
|
| 582 |
+
a_neurons = gr.Number(value=cfg.get("neurons", 768), label="Neurons", precision=0)
|
| 583 |
+
a_embed = gr.Number(value=cfg.get("embed_size", 384), label="Embed Size", precision=0)
|
| 584 |
+
a_dropout = gr.Number(value=cfg.get("dropout", 0.2), label="Dropout", precision=2)
|
| 585 |
+
a_working_mem = gr.Number(value=cfg.get("working_memory", 2048), label="Working Memory (0=off)", precision=0)
|
|
|
|
| 586 |
|
| 587 |
gr.Markdown("### Tags & Tokens")
|
| 588 |
with gr.Row():
|
| 589 |
+
a_user_tag = gr.Textbox(value=cfg.get("user_tag", "### Instruction:"), label="User Tag")
|
| 590 |
+
a_bot_tag = gr.Textbox(value=cfg.get("bot_tag", "### Response:"), label="Bot Tag")
|
| 591 |
+
a_eos = gr.Textbox(value=cfg.get("eos_token", "<|end|>"), label="EOS Token")
|
| 592 |
+
a_sys = gr.Textbox(value=cfg.get("system_prompt", "You are a helpful AI named Linny."), label="System Prompt", lines=2)
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
gr.Markdown("### Reasoning")
|
| 595 |
+
a_reasoning_mode = gr.Radio(choices=["prompt_suffix", "response_prefix"], value=cfg.get("reasoning_mode", "response_prefix"), label="Force Reasoning Mode")
|
| 596 |
+
a_reasoning_start = gr.Checkbox(value=cfg.get("reasoning_start", False), label="Enable Reasoning Start Prefix")
|
| 597 |
+
a_min_response_tokens = gr.Number(value=cfg.get("min_response_tokens", 3), label="Minimum Response Tokens After </think>", precision=0)
|
| 598 |
+
a_max_reasoning_tokens = gr.Number(value=cfg.get("max_reasoning_tokens", 2500), label="Max Reasoning Tokens", precision=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
|
| 600 |
gr.Markdown("### Default Generation Settings")
|
| 601 |
with gr.Row():
|
| 602 |
+
a_temp = gr.Slider(0.1, 1.5, value=cfg.get("default_temp", 0.8), step=0.05, label="Default Temperature")
|
| 603 |
+
a_penalty = gr.Slider(1.0, 2.0, value=cfg.get("default_penalty", 1.05), step=0.05, label="Default Penalty")
|
| 604 |
with gr.Row():
|
| 605 |
a_penalty_window = gr.Slider(10, 500, value=cfg.get("default_penalty_window", 110), step=10, label="Default Penalty Window")
|
| 606 |
with gr.Row():
|
| 607 |
+
a_topp = gr.Slider(0.0, 1.0, value=cfg.get("default_top_p", 0.4), step=0.05, label="Default Top-P")
|
| 608 |
+
a_topk = gr.Slider(0, 100, value=cfg.get("default_top_k", 65), step=1, label="Default Top-K")
|
| 609 |
+
a_maxlen = gr.Slider(100, 8000, value=cfg.get("default_max_len", 2600), step=50, label="Default Max Length")
|
| 610 |
|
| 611 |
save_cfg_btn = gr.Button("💾 Save config.json", variant="primary")
|
| 612 |
+
save_status = gr.Markdown("")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
|
| 614 |
+
# Callbacks
|
| 615 |
def do_admin_login(pw):
|
| 616 |
if pw == ADMIN_PASSWORD:
|
| 617 |
return gr.update(visible=True), "✅ Logged in."
|
| 618 |
return gr.update(visible=False), "❌ Incorrect password."
|
| 619 |
+
admin_login_btn.click(do_admin_login, inputs=[admin_pw], outputs=[admin_panel, admin_status])
|
|
|
|
|
|
|
| 620 |
|
| 621 |
def do_save_config(mtype, layers, neurons, embed, dropout, working_mem,
|
| 622 |
user_tag, bot_tag, eos, sys_prompt,
|
|
|
|
| 625 |
temp, penalty, penalty_window, top_p, top_k, max_len):
|
| 626 |
try:
|
| 627 |
new_cfg = {
|
| 628 |
+
"model_type": mtype,
|
| 629 |
+
"hidden_layers": int(layers),
|
| 630 |
+
"neurons": int(neurons),
|
| 631 |
+
"embed_size": int(embed),
|
| 632 |
+
"dropout": float(dropout),
|
| 633 |
+
"working_memory": int(working_mem),
|
| 634 |
+
"user_tag": user_tag,
|
| 635 |
+
"bot_tag": bot_tag,
|
| 636 |
+
"eos_token": eos,
|
| 637 |
+
"system_prompt": sys_prompt,
|
| 638 |
+
"reasoning_mode": reasoning_mode,
|
| 639 |
"reasoning_start": bool(reasoning_start),
|
| 640 |
"min_response_tokens": int(min_response_tokens),
|
| 641 |
"max_reasoning_tokens": int(max_reasoning_tokens),
|
| 642 |
+
"default_temp": float(temp),
|
| 643 |
"default_penalty": float(penalty),
|
| 644 |
"default_penalty_window": int(penalty_window),
|
| 645 |
+
"default_top_p": float(top_p),
|
| 646 |
+
"default_top_k": int(top_k),
|
| 647 |
"default_max_len": int(max_len),
|
| 648 |
}
|
| 649 |
save_config(new_cfg)
|
| 650 |
+
return "✅ config.json saved! Restart the Space to apply changes."
|
| 651 |
except Exception as e:
|
| 652 |
return f"❌ Error: {e}"
|
| 653 |
+
save_cfg_btn.click(do_save_config,
|
|
|
|
|
|
|
| 654 |
inputs=[model_type_admin, a_layers, a_neurons, a_embed, a_dropout, a_working_mem,
|
| 655 |
a_user_tag, a_bot_tag, a_eos, a_sys,
|
| 656 |
a_reasoning_mode, a_reasoning_start,
|
| 657 |
a_min_response_tokens, a_max_reasoning_tokens,
|
| 658 |
a_temp, a_penalty, a_penalty_window, a_topp, a_topk, a_maxlen],
|
| 659 |
+
outputs=[save_status])
|
|
|
|
| 660 |
|
| 661 |
def load_user_model(pt_file, tok_file, mtype, layers, neurons, embed, dropout,
|
| 662 |
+
working_mem, user_tag, bot_tag, eos):
|
| 663 |
if pt_file is None:
|
| 664 |
return None, None, "❌ Please upload a .pt file first."
|
| 665 |
try:
|
| 666 |
user_cfg = {
|
| 667 |
+
"model_type": mtype,
|
| 668 |
+
"hidden_layers": int(layers),
|
| 669 |
+
"neurons": int(neurons),
|
| 670 |
+
"embed_size": int(embed),
|
| 671 |
+
"dropout": float(dropout),
|
| 672 |
"working_memory": int(working_mem),
|
| 673 |
+
"user_tag": user_tag,
|
| 674 |
+
"bot_tag": bot_tag,
|
| 675 |
+
"eos_token": eos,
|
| 676 |
+
"reasoning_mode": cfg.get("reasoning_mode", "response_prefix"),
|
| 677 |
+
"reasoning_start": cfg.get("reasoning_start", False),
|
| 678 |
"min_response_tokens": cfg.get("min_response_tokens", 3),
|
| 679 |
"max_reasoning_tokens": cfg.get("max_reasoning_tokens", 2500),
|
| 680 |
}
|
| 681 |
tok_path = tok_file.name if tok_file else None
|
| 682 |
+
m = LinnyModel(pt_file.name, user_cfg, tokenizer_path=tok_path)
|
| 683 |
+
return m, user_cfg, f"✅ Model loaded! ({user_cfg['hidden_layers']}L × {user_cfg['neurons']}N, {mtype}, epoch {m.epoch})"
|
|
|
|
| 684 |
except Exception as e:
|
| 685 |
return None, None, f"❌ Error: {e}"
|
| 686 |
+
load_user_btn.click(load_user_model,
|
|
|
|
|
|
|
| 687 |
inputs=[user_pt, user_tok, model_type_radio,
|
| 688 |
u_layers, u_neurons, u_embed, u_dropout, u_working_mem,
|
| 689 |
u_user_tag, u_bot_tag, u_eos],
|
| 690 |
+
outputs=[session_model, session_cfg, user_load_status])
|
|
|
|
| 691 |
|
| 692 |
def respond(message, history, model, temp, penalty, penalty_window, top_p, top_k, max_len, force_thinking):
|
| 693 |
if not message.strip():
|
| 694 |
yield history, "", gr.update(visible=False)
|
| 695 |
return
|
|
|
|
| 696 |
if model is None:
|
| 697 |
+
yield history + [{"role":"user","content":message},{"role":"assistant","content":"⚠️ No model loaded."}], "", gr.update(visible=False)
|
| 698 |
return
|
| 699 |
|
| 700 |
history = history + [{"role":"user","content":message},{"role":"assistant","content":""}]
|
|
|
|
|
|
|
| 701 |
full_response = ""
|
| 702 |
+
stop = False
|
| 703 |
+
think_start = None
|
| 704 |
+
think_end = None
|
|
|
|
| 705 |
|
| 706 |
try:
|
| 707 |
for chunk in model.stream_generate(
|
|
|
|
| 709 |
temperature=float(temp),
|
| 710 |
max_len=int(max_len),
|
| 711 |
penalty=float(penalty),
|
| 712 |
+
penalty_window=int(penalty_window),
|
| 713 |
top_p=float(top_p),
|
| 714 |
top_k=int(top_k),
|
| 715 |
force_thinking=bool(force_thinking),
|
| 716 |
+
min_response_tokens=model.config.get("min_response_tokens", 3),
|
| 717 |
+
max_reasoning_tokens=model.config.get("max_reasoning_tokens", 2500),
|
| 718 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
full_response += chunk
|
| 720 |
+
# Stop at EOS if present
|
| 721 |
+
eos_str = model.config.get("eos_token", "<|end|>")
|
| 722 |
+
if eos_str in full_response:
|
| 723 |
+
full_response = full_response[:full_response.find(eos_str)]
|
| 724 |
if full_response.count("<think>") > full_response.count("</think>"):
|
| 725 |
full_response += "</think>"
|
| 726 |
else:
|
| 727 |
stop = True
|
| 728 |
|
| 729 |
thinking, visible, think_complete = parse_think_tags(full_response)
|
|
|
|
|
|
|
| 730 |
if thinking and think_start is None:
|
| 731 |
think_start = time.time()
|
| 732 |
if think_complete and think_end is None and think_start is not None:
|
| 733 |
think_end = time.time()
|
| 734 |
+
think_elapsed = (think_end - think_start) if (think_end and think_start) else (time.time() - think_start if think_start else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
current_topic = extract_current_topic(thinking) if thinking else "Reasoning..."
|
|
|
|
| 736 |
html_code, visible_clean = extract_html_canvas(visible)
|
| 737 |
+
canvas_update = gr.update(visible=bool(html_code), value=make_canvas_html(html_code) if html_code else "")
|
| 738 |
+
history[-1] = {"role":"assistant","content":format_message(visible_clean, thinking, think_complete, think_elapsed, current_topic)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
yield history, "", canvas_update
|
|
|
|
| 740 |
if stop:
|
| 741 |
break
|
|
|
|
| 742 |
except Exception as e:
|
| 743 |
history[-1] = {"role":"assistant","content":f"⚠️ Generation error: {e}"}
|
| 744 |
yield history, "", gr.update(visible=False)
|
|
|
|
| 749 |
if think_end is None and think_start is not None:
|
| 750 |
think_end = time.time()
|
| 751 |
think_elapsed = (think_end - think_start) if (think_end and think_start) else None
|
|
|
|
| 752 |
if think_complete and not visible.strip():
|
| 753 |
visible = "*(no response generated)*"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
current_topic = extract_current_topic(thinking) if thinking else "Reasoning..."
|
| 755 |
html_code, visible_clean = extract_html_canvas(visible)
|
| 756 |
+
history[-1] = {"role":"assistant","content":format_message(visible_clean, thinking, True, think_elapsed, current_topic)}
|
| 757 |
+
canvas_update = gr.update(visible=bool(html_code), value=make_canvas_html(html_code) if html_code else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
yield history, "", canvas_update
|
| 759 |
|
| 760 |
+
shared_inputs = [msg_box, chatbot, session_model,
|
| 761 |
+
temp_sl, penalty_sl, penalty_window_sl, topp_sl, topk_sl, max_len_sl, reasoning_toggle]
|
| 762 |
shared_outputs = [chatbot, msg_box, canvas_display]
|
| 763 |
+
send_btn.click(respond, inputs=shared_inputs, outputs=shared_outputs)
|
|
|
|
| 764 |
msg_box.submit(respond, inputs=shared_inputs, outputs=shared_outputs)
|
| 765 |
|
| 766 |
return demo
|
| 767 |
|
|
|
|
| 768 |
if __name__ == "__main__":
|
| 769 |
build_ui().launch(ssr_mode=False)
|