Daisuke675 commited on
Commit
afccf63
·
verified ·
1 Parent(s): 4859241

Upload 15_scugnizz_chat_gui_agent_v2_fixed.py with huggingface_hub

Browse files
15_scugnizz_chat_gui_agent_v2_fixed.py ADDED
@@ -0,0 +1,883 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # /// script
4
+ # dependencies = ["torch","datasets","transformers","huggingface_hub","numpy","hf_xet"]
5
+ # ///
6
+ # SFT ScugnizzLLM-1.3B PCS from 1B-token pretrain checkpoint.
7
+
8
+ import argparse, json, math, random, time, sys, os, re
9
+ from contextlib import nullcontext
10
+ from dataclasses import dataclass, asdict
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from datasets import load_dataset
18
+ from huggingface_hub import HfApi, upload_folder, hf_hub_download
19
+ from transformers import GPT2TokenizerFast
20
+
21
+
22
+ # ==========================================================
23
+ # MODEL
24
+ # ==========================================================
25
+
26
+ @dataclass
27
+ class GPTConfig:
28
+ vocab_size: int
29
+ block_size: int
30
+ n_layer: int
31
+ n_head: int
32
+ n_embd: int
33
+ dropout: float = 0.1
34
+ bias: bool = False
35
+ pcs_a: float = 0.8309193524478643
36
+ pcs_b: float = 0.0
37
+
38
+
39
+ class PCS(nn.Module):
40
+ def __init__(self, a=0.8309193524478643, b=0.0):
41
+ super().__init__()
42
+ self.a = float(a)
43
+ self.b = float(b)
44
+
45
+ def forward(self, x):
46
+ return x * torch.sin(self.a * x) + self.b * torch.cos(x)
47
+
48
+
49
+ class Attn(nn.Module):
50
+ def __init__(self, c):
51
+ super().__init__()
52
+ assert c.n_embd % c.n_head == 0
53
+ self.n_head = c.n_head
54
+ self.head_dim = c.n_embd // c.n_head
55
+ self.dropout = c.dropout
56
+ self.qkv = nn.Linear(c.n_embd, 3 * c.n_embd, bias=c.bias)
57
+ self.proj = nn.Linear(c.n_embd, c.n_embd, bias=c.bias)
58
+ self.drop = nn.Dropout(c.dropout)
59
+
60
+ def forward(self, x):
61
+ B, T, C = x.shape
62
+ q, k, v = self.qkv(x).split(C, dim=2)
63
+ q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
64
+ k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
65
+ v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
66
+ y = F.scaled_dot_product_attention(
67
+ q, k, v,
68
+ dropout_p=self.dropout if self.training else 0.0,
69
+ is_causal=True,
70
+ )
71
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
72
+ return self.drop(self.proj(y))
73
+
74
+
75
+ class MLP(nn.Module):
76
+ def __init__(self, c):
77
+ super().__init__()
78
+ self.fc1 = nn.Linear(c.n_embd, 4 * c.n_embd, bias=c.bias)
79
+ self.act = PCS(c.pcs_a, c.pcs_b)
80
+ self.fc2 = nn.Linear(4 * c.n_embd, c.n_embd, bias=c.bias)
81
+ self.drop = nn.Dropout(c.dropout)
82
+
83
+ def forward(self, x):
84
+ return self.drop(self.fc2(self.act(self.fc1(x))))
85
+
86
+
87
+ class Block(nn.Module):
88
+ def __init__(self, c):
89
+ super().__init__()
90
+ self.ln1 = nn.LayerNorm(c.n_embd)
91
+ self.attn = Attn(c)
92
+ self.ln2 = nn.LayerNorm(c.n_embd)
93
+ self.mlp = MLP(c)
94
+
95
+ def forward(self, x):
96
+ x = x + self.attn(self.ln1(x))
97
+ x = x + self.mlp(self.ln2(x))
98
+ return x
99
+
100
+
101
+ class GPT(nn.Module):
102
+ def __init__(self, c):
103
+ super().__init__()
104
+ self.cfg = c
105
+ self.tok_emb = nn.Embedding(c.vocab_size, c.n_embd)
106
+ self.pos_emb = nn.Embedding(c.block_size, c.n_embd)
107
+ self.drop = nn.Dropout(c.dropout)
108
+ self.blocks = nn.ModuleList([Block(c) for _ in range(c.n_layer)])
109
+ self.ln_f = nn.LayerNorm(c.n_embd)
110
+ self.lm_head = nn.Linear(c.n_embd, c.vocab_size, bias=False)
111
+ self.tok_emb.weight = self.lm_head.weight
112
+ self.apply(self._init)
113
+ for n, p in self.named_parameters():
114
+ if n.endswith("proj.weight") or n.endswith("fc2.weight"):
115
+ nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * c.n_layer))
116
+
117
+ def _init(self, m):
118
+ if isinstance(m, nn.Linear):
119
+ nn.init.normal_(m.weight, mean=0.0, std=0.02)
120
+ if m.bias is not None:
121
+ nn.init.zeros_(m.bias)
122
+ elif isinstance(m, nn.Embedding):
123
+ nn.init.normal_(m.weight, mean=0.0, std=0.02)
124
+
125
+ def forward(self, idx, targets=None):
126
+ B, T = idx.shape
127
+ if T > self.cfg.block_size:
128
+ idx = idx[:, -self.cfg.block_size:]
129
+ if targets is not None:
130
+ targets = targets[:, -self.cfg.block_size:]
131
+ B, T = idx.shape
132
+
133
+ pos = torch.arange(T, device=idx.device)
134
+ x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
135
+ for b in self.blocks:
136
+ x = b(x)
137
+ logits = self.lm_head(self.ln_f(x))
138
+
139
+ loss = None
140
+ if targets is not None:
141
+ loss = F.cross_entropy(
142
+ logits.reshape(B * T, -1),
143
+ targets.reshape(B * T),
144
+ ignore_index=-100,
145
+ )
146
+ return logits, loss
147
+
148
+
149
+ # ==========================================================
150
+ # UTILS
151
+ # ==========================================================
152
+
153
+ def args():
154
+ p = argparse.ArgumentParser()
155
+
156
+ # Base pretrain checkpoint
157
+ p.add_argument("--base-repo-id", default="Daisuke675/scugnizz-1b")
158
+ p.add_argument("--base-ckpt", default="AUTO")
159
+
160
+ # Dataset
161
+ p.add_argument("--dataset", default="Daisuke675/scugnizz-instruct-pro-50k")
162
+ p.add_argument("--split", default="train")
163
+
164
+ # Training
165
+ p.add_argument("--block-size", type=int, default=1024)
166
+ p.add_argument("--batch-size", type=int, default=1)
167
+ p.add_argument("--grad-accum", type=int, default=16)
168
+ p.add_argument("--epochs", type=int, default=1)
169
+ p.add_argument("--max-steps", type=int, default=0)
170
+ p.add_argument("--lr", type=float, default=5e-6)
171
+ p.add_argument("--min-lr", type=float, default=1e-6)
172
+ p.add_argument("--warmup-steps", type=int, default=50)
173
+ p.add_argument("--weight-decay", type=float, default=0.05)
174
+ p.add_argument("--grad-clip", type=float, default=1.0)
175
+ p.add_argument("--dropout", type=float, default=0.05)
176
+ p.add_argument("--eval-interval", type=int, default=250)
177
+ p.add_argument("--save-interval", type=int, default=250)
178
+ p.add_argument("--log-interval", type=int, default=10)
179
+ p.add_argument("--eval-batches", type=int, default=30)
180
+ p.add_argument("--seed", type=int, default=42)
181
+ p.add_argument("--dtype", choices=["auto", "bfloat16", "float16", "float32"], default="auto")
182
+
183
+ # Output
184
+ p.add_argument("--out-dir", default="runs/sft-1b-from-1b")
185
+ p.add_argument("--hub-repo-id", default="Daisuke675/scugnizz-1b")
186
+ p.add_argument("--hub-path", default="training-runs/sft-1b-pro-50k")
187
+ p.add_argument("--push", action="store_true", default=True)
188
+ p.add_argument("--push-every-save", action="store_true", default=True)
189
+
190
+ return p.parse_args()
191
+
192
+
193
+ def seed_all(s):
194
+ random.seed(s)
195
+ np.random.seed(s)
196
+ torch.manual_seed(s)
197
+ if torch.cuda.is_available():
198
+ torch.cuda.manual_seed_all(s)
199
+
200
+
201
+ def dtype_of(d, dev):
202
+ if d != "auto":
203
+ return d
204
+ if dev == "cuda" and torch.cuda.is_bf16_supported():
205
+ return "bfloat16"
206
+ if dev == "cuda":
207
+ return "float16"
208
+ return "float32"
209
+
210
+
211
+ def ac(dev, dt):
212
+ if dev != "cuda" or dt == "float32":
213
+ return nullcontext()
214
+ return torch.amp.autocast("cuda", dtype=torch.bfloat16 if dt == "bfloat16" else torch.float16)
215
+
216
+
217
+ def lr_at(step, max_steps, a):
218
+ if step < a.warmup_steps:
219
+ return a.lr * (step + 1) / max(1, a.warmup_steps)
220
+ r = (step - a.warmup_steps) / max(1, max_steps - a.warmup_steps)
221
+ return a.min_lr + 0.5 * (1 + math.cos(math.pi * r)) * (a.lr - a.min_lr)
222
+
223
+
224
+ def save(path, model, opt, step, best, a, cfg):
225
+ path.parent.mkdir(parents=True, exist_ok=True)
226
+ payload = {
227
+ "step": int(step),
228
+ "model": model.state_dict(),
229
+ "optimizer": opt.state_dict(),
230
+ "best_val": float(best),
231
+ "args": vars(a),
232
+ "config": asdict(cfg),
233
+ }
234
+ torch.save(payload, path)
235
+ print("SAVED", path, flush=True)
236
+
237
+
238
+ def upload(out, repo, path, msg):
239
+ api = HfApi()
240
+ api.create_repo(repo, repo_type="model", exist_ok=True)
241
+ upload_folder(
242
+ repo_id=repo,
243
+ repo_type="model",
244
+ folder_path=out,
245
+ path_in_repo=path,
246
+ commit_message=msg,
247
+ )
248
+ print("UPLOADED", repo, path, flush=True)
249
+
250
+
251
+ def resolve_base_checkpoint(repo_id, base_ckpt):
252
+ if base_ckpt and base_ckpt != "AUTO":
253
+ return hf_hub_download(repo_id=repo_id, filename=base_ckpt, repo_type="model"), base_ckpt
254
+
255
+ candidates = [
256
+ "training-runs/pretrain-1b/checkpoint_last.pt",
257
+ "training-runs/pretrain-1b-pcs-1b/checkpoint_last.pt",
258
+ "training-runs/pretrain-1b-1b-tokens/checkpoint_last.pt",
259
+ "training-runs/pretrain-1b-h200x8/checkpoint_last.pt",
260
+ "training-runs/pretrain/checkpoint_last.pt",
261
+ "checkpoint_last.pt",
262
+ ]
263
+
264
+ errors = []
265
+ for c in candidates:
266
+ try:
267
+ p = hf_hub_download(repo_id=repo_id, filename=c, repo_type="model")
268
+ print("BASE CHECKPOINT FOUND:", c, flush=True)
269
+ return p, c
270
+ except Exception as e:
271
+ errors.append((c, repr(e)))
272
+
273
+ print("ERRORE: checkpoint base 1.3B non trovato automaticamente.", flush=True)
274
+ print("Percorsi provati:", flush=True)
275
+ for c, e in errors:
276
+ print(" -", c, "=>", e[:200], flush=True)
277
+ print("Rilancia passando il percorso esatto, esempio:", flush=True)
278
+ print("hf jobs uv run --flavor h200 --secrets HF_TOKEN 10_sft_1b_pro_50k.py --base-ckpt training-runs/TUO_PERCORSO/checkpoint_last.pt", flush=True)
279
+ raise FileNotFoundError("Base checkpoint not found")
280
+
281
+
282
+ # ==========================================================
283
+ # DATASET FORMAT
284
+ # ==========================================================
285
+
286
+ def pick(row, names):
287
+ for n in names:
288
+ if n in row and row[n] is not None:
289
+ v = row[n]
290
+ if isinstance(v, str) and v.strip():
291
+ return v.strip()
292
+ return ""
293
+
294
+
295
+ def make_prompt_and_answer(row):
296
+ # Robust for common instruct schemas.
297
+ instruction = pick(row, ["instruction", "prompt", "question", "input", "query", "user"])
298
+ context = pick(row, ["context", "system", "source"])
299
+ answer = pick(row, ["output", "response", "answer", "completion", "assistant", "text"])
300
+
301
+ # If dataset has only text, split very lightly or train whole text.
302
+ if not answer and "text" in row and isinstance(row["text"], str):
303
+ txt = row["text"].strip()
304
+ return "", txt
305
+
306
+ if context:
307
+ prompt = f"### Instruction:\\n{instruction}\\n\\n### Context:\\n{context}\\n\\n### Response:\\n"
308
+ else:
309
+ prompt = f"### Instruction:\\n{instruction}\\n\\n### Response:\\n"
310
+
311
+ return prompt, answer
312
+
313
+
314
+ def encode_example(tok, row, block_size):
315
+ prompt, answer = make_prompt_and_answer(row)
316
+
317
+ # Train only the answer where possible.
318
+ full = prompt + answer + tok.eos_token
319
+
320
+ full_ids = tok.encode(full)
321
+ prompt_ids = tok.encode(prompt)
322
+
323
+ if len(full_ids) > block_size + 1:
324
+ # Keep the end, but avoid losing all answer labels where possible.
325
+ full_ids = full_ids[-(block_size + 1):]
326
+ # If prompt got truncated, no prompt mask.
327
+ prompt_len = 0
328
+ else:
329
+ prompt_len = min(len(prompt_ids), len(full_ids))
330
+
331
+ x = full_ids[:-1]
332
+ y = full_ids[1:]
333
+
334
+ # Mask prompt labels. y position i predicts full_ids[i+1].
335
+ # If i+1 is still inside prompt, ignore.
336
+ labels = []
337
+ for i, target in enumerate(y):
338
+ if (i + 1) < prompt_len:
339
+ labels.append(-100)
340
+ else:
341
+ labels.append(target)
342
+
343
+ return x, labels
344
+
345
+
346
+ class SFTDataset:
347
+ def __init__(self, rows, tok, block_size):
348
+ self.items = []
349
+ self.tok = tok
350
+ self.block_size = block_size
351
+
352
+ for row in rows:
353
+ x, y = encode_example(tok, row, block_size)
354
+ if len(x) >= 8 and any(t != -100 for t in y):
355
+ self.items.append((x, y))
356
+
357
+ if not self.items:
358
+ raise RuntimeError("No usable SFT examples found. Check dataset columns.")
359
+
360
+ def __len__(self):
361
+ return len(self.items)
362
+
363
+ def batch(self, batch_size, dev):
364
+ idxs = np.random.randint(0, len(self.items), size=(batch_size,))
365
+ xs, ys = [], []
366
+ max_len = 0
367
+ for i in idxs:
368
+ x, y = self.items[i]
369
+ max_len = max(max_len, len(x))
370
+ xs.append(x)
371
+ ys.append(y)
372
+
373
+ max_len = min(max_len, self.block_size)
374
+ pad_id = self.tok.eos_token_id
375
+
376
+ bx, by = [], []
377
+ for x, y in zip(xs, ys):
378
+ x = x[-max_len:]
379
+ y = y[-max_len:]
380
+ pad = max_len - len(x)
381
+ bx.append([pad_id] * pad + x)
382
+ by.append([-100] * pad + y)
383
+
384
+ return (
385
+ torch.tensor(bx, dtype=torch.long, device=dev),
386
+ torch.tensor(by, dtype=torch.long, device=dev),
387
+ )
388
+
389
+
390
+ @torch.no_grad()
391
+ def eval_model(model, data, a, dev, dt):
392
+ model.eval()
393
+ losses = []
394
+ for _ in range(a.eval_batches):
395
+ x, y = data.batch(a.batch_size, dev)
396
+ with ac(dev, dt):
397
+ _, loss = model(x, y)
398
+ losses.append(float(loss.item()))
399
+ model.train()
400
+ return float(np.mean(losses))
401
+
402
+
403
+ @torch.no_grad()
404
+ def quick_generate(model, tok, prompt, dev, dt, max_new=120):
405
+ model.eval()
406
+ ids = torch.tensor([tok.encode(prompt)], dtype=torch.long, device=dev)
407
+ for _ in range(max_new):
408
+ x = ids[:, -model.cfg.block_size:]
409
+ with ac(dev, dt):
410
+ logits, _ = model(x)
411
+ logits = logits[:, -1, :].float() / 0.8
412
+ probs = torch.softmax(logits, dim=-1)
413
+ sorted_probs, sorted_idx = torch.sort(probs, descending=True)
414
+ cum = torch.cumsum(sorted_probs, dim=-1)
415
+ mask = cum > 0.9
416
+ mask[..., 1:] = mask[..., :-1].clone()
417
+ mask[..., 0] = False
418
+ sorted_probs[mask] = 0
419
+ sorted_probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True)
420
+ sample = torch.multinomial(sorted_probs, 1)
421
+ nxt = sorted_idx.gather(-1, sample)
422
+ ids = torch.cat([ids, nxt], dim=1)
423
+ if int(nxt.item()) == tok.eos_token_id:
424
+ break
425
+ return tok.decode(ids[0].tolist(), skip_special_tokens=True)
426
+
427
+
428
+
429
+
430
+ # ==========================================================
431
+ # SCUGNIZZ AGENT MODE - ROUTER + MOCK TOOLS
432
+ # ==========================================================
433
+
434
+ AGENT_CITIES = [
435
+ "Udine","Trieste","Genova","Roma","Milano","Napoli","Torino","Bologna","Palermo","Cagliari",
436
+ "Verona","Bari","Lecce","Ancona","Aosta","Trento","Pordenone","Gorizia","Firenze","Padova"
437
+ ]
438
+ AGENT_STOCKS = ["TSLA","AAPL","MSFT","NVDA","AMD","VWCE","SPY","ENI","ISP","GOOGL","AMZN","META","BTC","ETH"]
439
+
440
+ def agent_json(obj):
441
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
442
+
443
+ def normalize_text(s):
444
+ return re.sub(r"\s+", " ", (s or "").strip())
445
+
446
+ def find_city(text):
447
+ low = text.lower()
448
+ for c in AGENT_CITIES:
449
+ if c.lower() in low:
450
+ return c
451
+ m = re.search(r"\b(?:a|di|per|in)\s+([A-ZÀ-Ü][a-zà-ü]+)", text)
452
+ return m.group(1) if m else None
453
+
454
+ def find_stock(text):
455
+ upper = text.upper()
456
+ for s in AGENT_STOCKS:
457
+ if re.search(rf"\b{re.escape(s)}\b", upper):
458
+ return s
459
+ m = re.search(r"\b([A-Z]{2,5})\b", upper)
460
+ return m.group(1) if m else None
461
+
462
+ def find_domain(text):
463
+ m = re.search(r"\b((?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})\b", text)
464
+ return m.group(1).lower() if m else None
465
+
466
+ def find_ip(text):
467
+ m = re.search(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)
468
+ return m.group(0) if m else None
469
+
470
+ def find_url(text):
471
+ m = re.search(r"https?://[^\s]+", text)
472
+ return m.group(0).rstrip(".,)") if m else None
473
+
474
+ def find_hash(text):
475
+ m = re.search(r"\b[a-fA-F0-9]{32}\b|\b[a-fA-F0-9]{40}\b|\b[a-fA-F0-9]{64}\b", text)
476
+ return m.group(0) if m else None
477
+
478
+ def route_user_to_tool(text):
479
+ t = normalize_text(text)
480
+ low = t.lower()
481
+
482
+ if any(k in low for k in ["meteo", "tempo fa", "weather", "prevision"]):
483
+ city = find_city(t)
484
+ if city:
485
+ return {"name": "weather.forecast", "arguments": {"city": city}}
486
+ return {"error": "Mi serve la città per usare il tool meteo."}
487
+
488
+ if any(k in low for k in ["quota", "quotazione", "prezzo", "stock", "azioni", "etf", "quote"]):
489
+ sym = find_stock(t)
490
+ if sym:
491
+ return {"name": "finance.quote", "arguments": {"symbol": sym}}
492
+ return {"error": "Mi serve il simbolo del titolo/ETF per usare il tool finanza."}
493
+
494
+ if any(k in low for k in ["canzone", "brano", "sto ascoltando", "current song", "musica"]):
495
+ return {"name": "spotify.current_song", "arguments": {}}
496
+
497
+ if any(k in low for k in ["appuntamento", "calendario", "agenda", "evento", "meeting"]):
498
+ return {"name": "calendar.next_event", "arguments": {}}
499
+
500
+ if any(k in low for k in ["email non lette", "mail non lette", "unread", "posta non letta"]):
501
+ return {"name": "unread_mail_count", "arguments": {}}
502
+
503
+ if any(k in low for k in ["dns", "record"]):
504
+ d = find_domain(t)
505
+ record = "A"
506
+ for rtype in ["AAAA", "TXT", "MX", "NS", "A"]:
507
+ if re.search(rf"\b{rtype}\b", t.upper()):
508
+ record = rtype
509
+ break
510
+ if d:
511
+ return {"name": "dns.lookup", "arguments": {"domain": d, "record_type": record}}
512
+ return {"error": "Mi serve il dominio per il lookup DNS."}
513
+
514
+ if any(k in low for k in ["dominio", "domain", "whois", "osint"]):
515
+ d = find_domain(t)
516
+ if d:
517
+ return {"name": "osint.domain_lookup", "arguments": {"domain": d}}
518
+ return {"error": "Mi serve il dominio da analizzare."}
519
+
520
+ if any(k in low for k in ["indirizzo ip", "lookup ip", "ip "]):
521
+ ip = find_ip(t)
522
+ if ip:
523
+ return {"name": "ip.lookup", "arguments": {"ip": ip}}
524
+ return {"error": "Mi serve un indirizzo IP valido."}
525
+
526
+ if any(k in low for k in ["url", "link", "scansiona", "reputazione"]):
527
+ u = find_url(t)
528
+ if u:
529
+ return {"name": "url.scan", "arguments": {"url": u}}
530
+ return {"error": "Mi serve un URL completo da scansionare."}
531
+
532
+ if any(k in low for k in ["hash", "sha256", "md5", "sha1"]):
533
+ h = find_hash(t)
534
+ if h:
535
+ return {"name": "hash.lookup", "arguments": {"hash": h}}
536
+ return {"error": "Mi serve un hash valido."}
537
+
538
+ if any(k in low for k in ["sensore", "porta", "garage", "luce", "termostato", "telecamera"]):
539
+ room = None
540
+ device = None
541
+ for r in ["garage", "cucina", "salotto", "camera", "studio", "ingresso", "cantina", "terrazzo"]:
542
+ if r in low:
543
+ room = r
544
+ break
545
+ for d in ["sensore porta", "porta ingresso", "luce", "termostato", "telecamera", "sirena", "presa smart"]:
546
+ if d in low:
547
+ device = d
548
+ break
549
+ if device:
550
+ args = {"device": device}
551
+ if room:
552
+ args["room"] = room
553
+ return {"name": "home.sensor", "arguments": args}
554
+
555
+ return None
556
+
557
+ def execute_mock_tool(call):
558
+ name = call.get("name")
559
+ args = call.get("arguments") or {}
560
+
561
+ if name == "weather.forecast":
562
+ city = args.get("city", "Genova")
563
+ table = {
564
+ "Genova": ("pioggia", 24, 18),
565
+ "Udine": ("nuvoloso", 23, 9),
566
+ "Roma": ("sereno", 31, 8),
567
+ "Milano": ("caldo", 30, 7),
568
+ "Trento": ("neve", -2, 18),
569
+ }
570
+ cond, temp, wind = table.get(city, ("sereno", 25, 10))
571
+ return {"tool": name, "result": {"city": city, "condition": cond, "temperature_c": temp, "wind_kmh": wind}}
572
+
573
+ if name == "finance.quote":
574
+ sym = args.get("symbol", "TSLA")
575
+ prices = {"TSLA": 312.45, "MSFT": 512.34, "AMD": 165.77, "NVDA": 142.10, "VWCE": 127.45}
576
+ return {"tool": name, "result": {"symbol": sym, "price": prices.get(sym, 100.00), "currency": "USD" if sym != "VWCE" else "EUR", "change_percent": 0.8}}
577
+
578
+ if name == "spotify.current_song":
579
+ return {"tool": name, "result": {"artist": "Muse", "title": "Uprising", "album": "The Resistance"}}
580
+
581
+ if name == "calendar.next_event":
582
+ return {"tool": name, "result": {"title": "Manutenzione", "date": "domani", "time": "15:45"}}
583
+
584
+ if name == "unread_mail_count":
585
+ return {"tool": name, "result": {"unread": 7, "important": 2, "latest_sender": "Anna"}}
586
+
587
+ if name == "osint.domain_lookup":
588
+ domain = args.get("domain", "example.com")
589
+ return {"tool": name, "result": {"domain": domain, "status": "mock", "risk": "unknown", "note": "Risultato dimostrativo locale"}}
590
+
591
+ if name == "dns.lookup":
592
+ return {"tool": name, "result": {"domain": args.get("domain"), "record_type": args.get("record_type", "A"), "answers": ["93.184.216.34"]}}
593
+
594
+ if name == "ip.lookup":
595
+ return {"tool": name, "result": {"ip": args.get("ip"), "asn": "AS15169", "country": "US", "provider": "mock"}}
596
+
597
+ if name == "url.scan":
598
+ return {"tool": name, "result": {"url": args.get("url"), "verdict": "unknown", "risk_score": 0, "note": "Mock scan locale"}}
599
+
600
+ if name == "hash.lookup":
601
+ return {"tool": name, "result": {"hash": args.get("hash"), "verdict": "unknown", "engines_detected": 0}}
602
+
603
+ if name == "home.sensor":
604
+ return {"tool": name, "result": {"device": args.get("device", "sensore"), "room": args.get("room", "ingresso"), "state": "aperta"}}
605
+
606
+ return {"tool": name or "unknown", "error": "Tool non implementato", "arguments": args}
607
+
608
+ def build_tool_result_prompt(tool_result):
609
+ return (
610
+ "### Instruction:\n"
611
+ "TOOL_RESULT:\n"
612
+ + agent_json(tool_result)
613
+ + "\n\nScrivi una risposta naturale in italiano usando solo i dati utili. Non inventare nulla.\n\n"
614
+ "### Response:\n"
615
+ )
616
+
617
+ # ==========================================================
618
+ # SCUGNIZZLLM CHAT GUI
619
+ # ==========================================================
620
+ import threading
621
+ import tkinter as tk
622
+ from tkinter import ttk, scrolledtext, messagebox, filedialog
623
+
624
+ CHECKPOINTS = {
625
+ "Toolcalling only best": "training-runs/sft-toolcalling-only/checkpoint_best.pt",
626
+ "Toolcalling only last": "training-runs/sft-toolcalling-only/checkpoint_last.pt",
627
+ "V3 agentic smart mix best": "training-runs/sft-universal-tool-renderer-1b-v3-agentic-smart-mix/checkpoint_best.pt",
628
+ "SFT 1.32B best": "training-runs/sft-1b-pro-50k-1320m/checkpoint_best.pt",
629
+ "SFT 1.32B last": "training-runs/sft-1b-pro-50k-1320m/checkpoint_last.pt",
630
+ "SFT 1B best": "training-runs/sft-1b-pro-50k/checkpoint_best.pt",
631
+ "SFT 1B last": "training-runs/sft-1b-pro-50k/checkpoint_last.pt",
632
+ "Pretrain 1.32B last": "training-runs/pretrain-1b/checkpoint_last.pt",
633
+ }
634
+ REPO_ID = "ProjectScugnizz/scugnizz-1b"
635
+
636
+ @torch.no_grad()
637
+ def sample_next_token(logits, temperature=0.7, top_p=0.9, top_k=50, repetition_penalty=1.10, recent_tokens=None):
638
+ logits = logits.float()
639
+ if recent_tokens and repetition_penalty and repetition_penalty > 1.0:
640
+ for t in set(recent_tokens[-128:]):
641
+ logits[:, t] /= repetition_penalty
642
+ if temperature <= 0:
643
+ return torch.argmax(logits, dim=-1, keepdim=True)
644
+ logits = logits / max(temperature, 1e-6)
645
+ if top_k and top_k > 0:
646
+ vals, idx = torch.topk(logits, min(top_k, logits.size(-1)))
647
+ mask = torch.full_like(logits, float("-inf"))
648
+ logits = mask.scatter(-1, idx, vals)
649
+ probs = torch.softmax(logits, dim=-1)
650
+ if top_p and top_p < 1.0:
651
+ sorted_probs, sorted_idx = torch.sort(probs, descending=True)
652
+ cum = torch.cumsum(sorted_probs, dim=-1)
653
+ mask = cum > top_p
654
+ mask[..., 1:] = mask[..., :-1].clone()
655
+ mask[..., 0] = False
656
+ sorted_probs[mask] = 0
657
+ sorted_probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True)
658
+ sample = torch.multinomial(sorted_probs, 1)
659
+ return sorted_idx.gather(-1, sample)
660
+ return torch.multinomial(probs, 1)
661
+
662
+ class ScugnizzEngine:
663
+ def __init__(self):
664
+ self.model = None
665
+ self.tok = None
666
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
667
+ self.dt = dtype_of("auto", "cuda" if torch.cuda.is_available() else "cpu")
668
+ self.loaded_name = None
669
+ def load(self, checkpoint_name):
670
+ ckpt_file = CHECKPOINTS[checkpoint_name]
671
+ self.tok = GPT2TokenizerFast.from_pretrained("gpt2")
672
+ self.tok.pad_token = self.tok.eos_token
673
+ path = hf_hub_download(repo_id=REPO_ID, filename=ckpt_file, repo_type="model")
674
+ ck = torch.load(path, map_location="cpu")
675
+ if isinstance(ck, dict) and "config" in ck:
676
+ cfg = GPTConfig(**ck["config"])
677
+ else:
678
+ cfg = GPTConfig(vocab_size=self.tok.vocab_size, block_size=1024, n_layer=24, n_head=16, n_embd=2048, dropout=0.0, bias=False, pcs_a=0.8309193524478643, pcs_b=0.0)
679
+ cfg.dropout = 0.0
680
+ model = GPT(cfg)
681
+ sd = ck["model"] if isinstance(ck, dict) and "model" in ck else ck
682
+ if any(k.startswith("module.") for k in sd.keys()):
683
+ sd = {k.replace("module.", "", 1): v for k, v in sd.items()}
684
+ model.load_state_dict(sd, strict=True)
685
+ model.to(self.device)
686
+ model.eval()
687
+ self.model = model
688
+ self.loaded_name = checkpoint_name
689
+ def build_prompt(self, history, user_msg):
690
+ text = ""
691
+ for role, msg in history[-8:]:
692
+ if role == "user":
693
+ text += f"### Instruction:\n{msg}\n\n### Response:\n"
694
+ else:
695
+ text += f"{msg}\n\n"
696
+ text += f"### Instruction:\n{user_msg}\n\n### Response:\n"
697
+ return text
698
+ @torch.no_grad()
699
+ def generate_stream(self, prompt, max_new=256, temperature=0.7, top_p=0.9, top_k=50, repetition_penalty=1.10, stop_flag=None):
700
+ if self.model is None:
701
+ raise RuntimeError("Modello non caricato.")
702
+ ids = torch.tensor([self.tok.encode(prompt)], dtype=torch.long, device=self.device)
703
+ generated = []
704
+ recent = ids[0].tolist()[-128:]
705
+ for _ in range(max_new):
706
+ if stop_flag and stop_flag():
707
+ break
708
+ x = ids[:, -self.model.cfg.block_size:]
709
+ with ac("cuda" if torch.cuda.is_available() else "cpu", self.dt):
710
+ logits, _ = self.model(x)
711
+ nxt = sample_next_token(logits[:, -1, :], temperature=temperature, top_p=top_p, top_k=top_k, repetition_penalty=repetition_penalty, recent_tokens=recent)
712
+ token_id = int(nxt.item())
713
+ if token_id == self.tok.eos_token_id:
714
+ break
715
+ ids = torch.cat([ids, nxt], dim=1)
716
+ recent.append(token_id)
717
+ generated.append(token_id)
718
+ piece = self.tok.decode([token_id], skip_special_tokens=True)
719
+ yield piece
720
+ current = self.tok.decode(generated, skip_special_tokens=True)
721
+ if "\n### Instruction:" in current or "\n### Response:" in current:
722
+ break
723
+
724
+ class ChatApp:
725
+ def __init__(self, root):
726
+ self.root = root
727
+ self.root.title("ScugnizzLLM Chat")
728
+ self.root.geometry("1100x760")
729
+ self.root.configure(bg="#151515")
730
+ self.engine = ScugnizzEngine()
731
+ self.history = []
732
+ self.stop = False
733
+ self.generating = False
734
+ self.setup_style()
735
+ self.build_ui()
736
+ def setup_style(self):
737
+ style = ttk.Style()
738
+ try: style.theme_use("clam")
739
+ except Exception: pass
740
+ style.configure("TFrame", background="#151515")
741
+ style.configure("TLabel", background="#151515", foreground="#e8e8e8")
742
+ style.configure("TButton", padding=6)
743
+ def build_ui(self):
744
+ top = ttk.Frame(self.root); top.pack(fill="x", padx=10, pady=8)
745
+ ttk.Label(top, text="Checkpoint:").pack(side="left")
746
+ self.ckpt_var = tk.StringVar(value="V3 agentic smart mix best")
747
+ ttk.Combobox(top, textvariable=self.ckpt_var, values=list(CHECKPOINTS.keys()), width=32, state="readonly").pack(side="left", padx=6)
748
+ self.load_btn = ttk.Button(top, text="Carica modello", command=self.load_model); self.load_btn.pack(side="left", padx=4)
749
+ ttk.Button(top, text="Nuova chat", command=self.new_chat).pack(side="left", padx=4)
750
+ ttk.Button(top, text="Salva chat", command=self.save_chat).pack(side="left", padx=4)
751
+ self.status = ttk.Label(top, text=f"Device: {self.engine.device}"); self.status.pack(side="right")
752
+ params = ttk.Frame(self.root); params.pack(fill="x", padx=10, pady=4)
753
+ self.temp=tk.DoubleVar(value=0.7); self.topp=tk.DoubleVar(value=0.9); self.topk=tk.IntVar(value=50); self.maxnew=tk.IntVar(value=256); self.rep=tk.DoubleVar(value=1.10); self.agent_mode=tk.BooleanVar(value=True); self.show_tool_log=tk.BooleanVar(value=True)
754
+ for label,var in [("Temp",self.temp),("Top-p",self.topp),("Top-k",self.topk),("Max token",self.maxnew),("Repetition",self.rep)]:
755
+ f=ttk.Frame(params); f.pack(side="left", padx=8); ttk.Label(f,text=label).pack(); ttk.Entry(f,textvariable=var,width=8).pack()
756
+ ttk.Checkbutton(params, text="Agent mode", variable=self.agent_mode).pack(side="left", padx=10)
757
+ ttk.Checkbutton(params, text="Tool log", variable=self.show_tool_log).pack(side="left", padx=10)
758
+ self.chat = scrolledtext.ScrolledText(self.root, wrap="word", bg="#101010", fg="#eeeeee", insertbackground="#ffffff", font=("Consolas", 11), padx=12, pady=12)
759
+ self.chat.pack(fill="both", expand=True, padx=10, pady=8)
760
+ self.chat.tag_config("user", foreground="#8fd3ff"); self.chat.tag_config("bot", foreground="#b6ffb6"); self.chat.tag_config("sys", foreground="#ffdf80"); self.chat.tag_config("tool", foreground="#ff9bd5")
761
+ self.append("ScugnizzLLM Chat pronto. Carica un checkpoint e scrivi una domanda.\n", "sys")
762
+ bottom = ttk.Frame(self.root); bottom.pack(fill="x", padx=10, pady=8)
763
+ self.input = tk.Text(bottom, height=4, bg="#202020", fg="#ffffff", insertbackground="#ffffff", font=("Consolas", 11)); self.input.pack(side="left", fill="x", expand=True)
764
+ self.input.bind("<Control-Return>", lambda e: self.send())
765
+ btns=ttk.Frame(bottom); btns.pack(side="right", padx=8)
766
+ self.send_btn=ttk.Button(btns, text="Invia\nCtrl+Invio", command=self.send); self.send_btn.pack(fill="x", pady=2)
767
+ ttk.Button(btns, text="Stop", command=self.stop_generation).pack(fill="x", pady=2)
768
+ def append(self, text, tag=None):
769
+ self.chat.insert("end", text, tag); self.chat.see("end"); self.root.update_idletasks()
770
+ def load_model(self):
771
+ name=self.ckpt_var.get(); self.status.config(text=f"Carico {name}..."); self.load_btn.config(state="disabled")
772
+ def worker():
773
+ try:
774
+ self.engine.load(name)
775
+ self.root.after(0, lambda: self.append(f"\n[OK] Modello caricato: {name}\n", "sys"))
776
+ self.root.after(0, lambda: self.status.config(text=f"Caricato: {name} | {self.engine.device}"))
777
+ except Exception as e:
778
+ self.root.after(0, lambda: messagebox.showerror("Errore caricamento", str(e)))
779
+ self.root.after(0, lambda: self.status.config(text="Errore caricamento"))
780
+ finally:
781
+ self.root.after(0, lambda: self.load_btn.config(state="normal"))
782
+ threading.Thread(target=worker, daemon=True).start()
783
+
784
+ def generate_text_blocking(self, prompt, max_new=None, temperature=None):
785
+ answer = ""
786
+ for piece in self.engine.generate_stream(
787
+ prompt,
788
+ max_new=int(max_new if max_new is not None else self.maxnew.get()),
789
+ temperature=float(temperature if temperature is not None else self.temp.get()),
790
+ top_p=float(self.topp.get()),
791
+ top_k=int(self.topk.get()),
792
+ repetition_penalty=float(self.rep.get()),
793
+ stop_flag=lambda: self.stop,
794
+ ):
795
+ answer += piece
796
+ return answer.strip()
797
+
798
+ def run_agent_mode(self, user_msg):
799
+ routed = route_user_to_tool(user_msg)
800
+
801
+ if isinstance(routed, dict) and "error" in routed:
802
+ return routed["error"], "[router] " + routed["error"]
803
+
804
+ if isinstance(routed, dict) and "name" in routed:
805
+ tool_result = execute_mock_tool(routed)
806
+ tool_log = (
807
+ "[router] TOOL_CALL "
808
+ + json.dumps(routed, ensure_ascii=False)
809
+ + "\nTOOL_RESULT "
810
+ + json.dumps(tool_result, ensure_ascii=False)
811
+ )
812
+ final_prompt = build_tool_result_prompt(tool_result)
813
+ final_answer = self.generate_text_blocking(final_prompt, max_new=int(self.maxnew.get()), temperature=0.05)
814
+ if not final_answer:
815
+ final_answer = json.dumps(tool_result, ensure_ascii=False)
816
+ return final_answer, tool_log
817
+
818
+ # Nessun tool: normale chat col modello
819
+ normal_prompt = self.engine.build_prompt(self.history, user_msg)
820
+ final_answer = self.generate_text_blocking(normal_prompt)
821
+ return final_answer, "[agent] Nessun tool necessario."
822
+
823
+ def send(self):
824
+ if self.generating:
825
+ return
826
+ msg = self.input.get("1.0", "end").strip()
827
+ if not msg:
828
+ return
829
+ if self.engine.model is None:
830
+ messagebox.showwarning("Modello non caricato", "Prima carica un checkpoint.")
831
+ return
832
+
833
+ self.input.delete("1.0", "end")
834
+ self.append(f"\nTu: {msg}\n", "user")
835
+ self.append("Scugnizz: ", "bot")
836
+
837
+ self.stop = False
838
+ self.generating = True
839
+ self.send_btn.config(state="disabled")
840
+
841
+ def worker():
842
+ answer = ""
843
+ try:
844
+ if self.agent_mode.get():
845
+ answer, tool_log = self.run_agent_mode(msg)
846
+ if self.show_tool_log.get() and tool_log:
847
+ self.root.after(0, lambda tl=tool_log: self.append("\n[TOOL LOG]\n" + tl + "\n\n", "tool"))
848
+ self.root.after(0, lambda a=answer: self.append(a, "bot"))
849
+ else:
850
+ prompt = self.engine.build_prompt(self.history, msg)
851
+ for piece in self.engine.generate_stream(
852
+ prompt,
853
+ max_new=int(self.maxnew.get()),
854
+ temperature=float(self.temp.get()),
855
+ top_p=float(self.topp.get()),
856
+ top_k=int(self.topk.get()),
857
+ repetition_penalty=float(self.rep.get()),
858
+ stop_flag=lambda: self.stop,
859
+ ):
860
+ answer += piece
861
+ self.root.after(0, lambda p=piece: self.append(p, "bot"))
862
+
863
+ self.history.append(("user", msg))
864
+ self.history.append(("assistant", answer.strip()))
865
+ self.root.after(0, lambda: self.append("\n", "bot"))
866
+
867
+ except Exception as e:
868
+ self.root.after(0, lambda e=e: self.append(f"\n[ERRORE] {e}\n", "sys"))
869
+ finally:
870
+ self.generating = False
871
+ self.root.after(0, lambda: self.send_btn.config(state="normal"))
872
+
873
+ threading.Thread(target=worker, daemon=True).start()
874
+
875
+ def stop_generation(self): self.stop=True
876
+ def new_chat(self): self.history.clear(); self.chat.delete("1.0", "end"); self.append("Nuova chat.\n", "sys")
877
+ def save_chat(self):
878
+ path=filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text", "*.txt"), ("Markdown", "*.md")])
879
+ if path:
880
+ Path(path).write_text(self.chat.get("1.0", "end"), encoding="utf-8"); self.append(f"\n[OK] Chat salvata: {path}\n", "sys")
881
+
882
+ if __name__ == "__main__":
883
+ root = tk.Tk(); app = ChatApp(root); root.mainloop()