Luke A Kist commited on
Commit
43321dd
·
verified ·
1 Parent(s): c16617e

Upload chat.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. chat.py +226 -0
chat.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AAC Micro Brain — Interactive Chat
4
+ Generates conversational responses from the trained MicroBrain model.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import mlx.core as mx
10
+ import mlx.nn as nn
11
+ from model import MicroBrain
12
+
13
+ PAD, BOS, EOS, SEP, UNK = 0, 1, 2, 3, 4
14
+
15
+ # Default to v3 checkpoint (all phases)
16
+ CHECKPOINT_DIR = "/Volumes/PRO-G40/models/aac-micro-brain/checkpoints"
17
+
18
+
19
+ class SimpleTokenizer:
20
+ def __init__(self):
21
+ self.word2idx = {"<pad>": 0, "<bos>": 1, "<eos>": 2, "<sep>": 3, "<unk>": 4}
22
+ self.idx2word = {v: k for k, v in self.word2idx.items()}
23
+
24
+ def encode(self, text):
25
+ return [self.word2idx.get(w, UNK) for w in re.findall(r"[a-z']+|[.,!?]", text.lower())]
26
+
27
+ def decode(self, ids):
28
+ return " ".join(self.idx2word.get(i, "?") for i in ids if i > 4)
29
+
30
+ @classmethod
31
+ def load(cls, path):
32
+ tok = cls()
33
+ with open(path) as f:
34
+ tok.word2idx = json.load(f)
35
+ tok.idx2word = {v: k for k, v in tok.word2idx.items()}
36
+ return tok
37
+
38
+ @property
39
+ def vocab_size(self):
40
+ return len(self.word2idx)
41
+
42
+
43
+ def generate_greedy(model, tokenizer, prompt, max_tokens=20):
44
+ tokens = [BOS] + tokenizer.encode(prompt) + [SEP]
45
+ for _ in range(max_tokens):
46
+ x = mx.array([tokens])
47
+ logits = model(x)
48
+ next_token = mx.argmax(logits[0, -1, :]).item()
49
+ if next_token in (PAD, EOS, SEP):
50
+ break
51
+ tokens.append(next_token)
52
+ sep_idx = tokens.index(SEP) + 1 if SEP in tokens else 0
53
+ return tokenizer.decode(tokens[sep_idx:])
54
+
55
+
56
+ def generate_sample(model, tokenizer, prompt, max_tokens=20, temperature=0.7, top_k=5):
57
+ tokens = [BOS] + tokenizer.encode(prompt) + [SEP]
58
+ for _ in range(max_tokens):
59
+ x = mx.array([tokens])
60
+ logits = model(x)
61
+ next_logits = logits[0, -1, :]
62
+ if top_k > 0 and top_k < next_logits.shape[0]:
63
+ top_k_indices = mx.argpartition(next_logits, kth=-top_k)[-top_k:]
64
+ mask = mx.full(next_logits.shape, float('-inf'))
65
+ mask[top_k_indices] = next_logits[top_k_indices]
66
+ next_logits = mask
67
+ next_logits = next_logits / temperature
68
+ probs = mx.softmax(next_logits, axis=-1)
69
+ next_token = mx.random.categorical(probs).item()
70
+ if next_token in (PAD, EOS, SEP):
71
+ break
72
+ tokens.append(next_token)
73
+ sep_idx = tokens.index(SEP) + 1 if SEP in tokens else 0
74
+ return tokenizer.decode(tokens[sep_idx:])
75
+
76
+
77
+ def generate_suggestions(model, tokenizer, prompt, n=6):
78
+ """Generate multiple unique response suggestions."""
79
+ suggestions = []
80
+ seen = set()
81
+
82
+ # Always include greedy
83
+ greedy = generate_greedy(model, tokenizer, prompt)
84
+ if greedy:
85
+ suggestions.append(greedy)
86
+ seen.add(greedy.lower())
87
+
88
+ # Sample diverse options
89
+ for temp in [0.5, 0.7, 0.9, 1.0, 1.2, 1.5]:
90
+ for k in [3, 5, 8]:
91
+ if len(suggestions) >= n:
92
+ break
93
+ s = generate_sample(model, tokenizer, prompt, temperature=temp, top_k=k)
94
+ if s and s.lower() not in seen:
95
+ suggestions.append(s)
96
+ seen.add(s.lower())
97
+ if len(suggestions) >= n:
98
+ break
99
+
100
+ return suggestions[:n]
101
+
102
+
103
+ def find_checkpoint():
104
+ """Find the best available checkpoint."""
105
+ import os
106
+ # Check for v3 meta to see if training completed
107
+ v3_meta = os.path.join(CHECKPOINT_DIR, "v3_meta.json")
108
+ if os.path.exists(v3_meta):
109
+ candidates = [
110
+ ("v3_best.safetensors", "v3_tokenizer.json", "v3 (all phases)"),
111
+ ("full_best.safetensors", "full_tokenizer.json", "v2 (phase 1+2)"),
112
+ ]
113
+ else:
114
+ # v3 still training — prefer v2 which is complete
115
+ candidates = [
116
+ ("full_best.safetensors", "full_tokenizer.json", "v2 (phase 1+2)"),
117
+ ("v3_best.safetensors", "v3_tokenizer.json", "v3 (training...)"),
118
+ ]
119
+ candidates.append(("curriculum_best.safetensors", "curriculum_tokenizer.json", "curriculum"))
120
+ for weights, tok, desc in candidates:
121
+ wp = os.path.join(CHECKPOINT_DIR, weights)
122
+ tp = os.path.join(CHECKPOINT_DIR, tok)
123
+ if os.path.exists(wp) and os.path.exists(tp):
124
+ return wp, tp, desc
125
+ return None, None, None
126
+
127
+
128
+ def load_model_config(tokenizer_path):
129
+ """Infer model config from metadata or tokenizer."""
130
+ import os
131
+ meta_candidates = [
132
+ os.path.join(CHECKPOINT_DIR, "v3_meta.json"),
133
+ os.path.join(CHECKPOINT_DIR, "full_meta.json"),
134
+ ]
135
+ for mp in meta_candidates:
136
+ if os.path.exists(mp):
137
+ with open(mp) as f:
138
+ meta = json.load(f)
139
+ vs = meta.get("vocab_size", 0)
140
+ np_ = meta.get("n_params", 0)
141
+ if vs and np_:
142
+ return vs, np_
143
+
144
+ # Infer from tokenizer
145
+ tok = SimpleTokenizer.load(tokenizer_path)
146
+ return tok.vocab_size, 0
147
+
148
+
149
+ def main():
150
+ weights_path, tok_path, desc = find_checkpoint()
151
+ if not weights_path:
152
+ print("No checkpoint found! Train a model first.")
153
+ return
154
+
155
+ print("=" * 50)
156
+ print(" AAC Micro Brain — Chat")
157
+ print("=" * 50)
158
+
159
+ print(f"\n Loading: {desc}")
160
+ tokenizer = SimpleTokenizer.load(tok_path)
161
+ vocab_size = tokenizer.vocab_size
162
+ print(f" Vocab: {vocab_size} words")
163
+
164
+ # Auto-detect model architecture from param count
165
+ # Try to load metadata
166
+ import os
167
+ meta_path = weights_path.replace("_best.safetensors", "_meta.json")
168
+ n_params = 0
169
+ if os.path.exists(meta_path):
170
+ with open(meta_path) as f:
171
+ meta = json.load(f)
172
+ n_params = meta.get("n_params", 0)
173
+
174
+ # Choose architecture based on param count or vocab size
175
+ if n_params > 15_000_000 or vocab_size > 5500:
176
+ d, h, L, dff = 512, 8, 6, 1024
177
+ elif n_params > 6_000_000 or vocab_size > 4000:
178
+ d, h, L, dff = 384, 6, 5, 768
179
+ elif n_params > 2_000_000:
180
+ d, h, L, dff = 256, 4, 4, 512
181
+ elif n_params > 500_000:
182
+ d, h, L, dff = 128, 4, 3, 256
183
+ else:
184
+ d, h, L, dff = 64, 2, 2, 128
185
+
186
+ model = MicroBrain(
187
+ vocab_size=vocab_size,
188
+ d_model=d, n_heads=h, n_layers=L, d_ff=dff,
189
+ max_seq_len=32,
190
+ )
191
+ model.load_weights(weights_path)
192
+ mx.eval(model.parameters())
193
+
194
+ from mlx.utils import tree_flatten
195
+ actual_params = sum(v.size for _, v in tree_flatten(model.parameters()))
196
+ print(f" Model: {actual_params:,} params ({actual_params/1e6:.1f}M)")
197
+ print(f" Architecture: d={d} h={h} L={L}")
198
+
199
+ print("\n Type a phrase. The model suggests responses.")
200
+ print(" Type 'quit' to exit.\n" + "-" * 50)
201
+
202
+ while True:
203
+ try:
204
+ user_input = input("\n>> ").strip()
205
+ except (EOFError, KeyboardInterrupt):
206
+ print("\nBye!")
207
+ break
208
+
209
+ if not user_input or user_input.lower() in ("quit", "exit", "q"):
210
+ print("Bye!")
211
+ break
212
+
213
+ # Greedy response
214
+ greedy = generate_greedy(model, tokenizer, user_input)
215
+ print(f"\n Best: {greedy}")
216
+
217
+ # Multiple suggestions
218
+ suggestions = generate_suggestions(model, tokenizer, user_input, n=6)
219
+ if len(suggestions) > 1:
220
+ print(" Alternatives:")
221
+ for i, s in enumerate(suggestions[1:], 2):
222
+ print(f" {i}. {s}")
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()