qwen2
ChoruYt commited on
Commit
5e7ce6a
Β·
verified Β·
1 Parent(s): 301ea5c

ExocoreV1: multi-thinking, web search, auto-load, loss chart, architecture tree, no system prompt

Browse files
Files changed (6) hide show
  1. exocore_model.py +165 -0
  2. main.py +238 -257
  3. replit.md +91 -91
  4. templates/index.html +565 -0
  5. templates/train.html +401 -0
  6. trainer.py +152 -467
exocore_model.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+
6
+ IM_START = 151644
7
+ IM_END = 151645
8
+ EOT = 151643
9
+ THINK_END = 151668
10
+
11
+ EXOCORE_IDENTITY = (
12
+ "You are ExocoreV1, a highly capable AI built from scratch by Exocore. "
13
+ "You are knowledgeable, analytical, direct, and precise. "
14
+ "You reason carefully before answering. "
15
+ "You identify yourself only as ExocoreV1."
16
+ )
17
+
18
+
19
+ def build_chat_prompt(messages, think=False, prior_thinking=None, search_context=None):
20
+ identity = EXOCORE_IDENTITY
21
+ if search_context:
22
+ identity += f"\n\nResearch context:\n{search_context}"
23
+
24
+ prompt = f"<|im_start|>system\n{identity}<|im_end|>\n"
25
+ for msg in messages:
26
+ role = msg.get("role", "user")
27
+ content = msg.get("content", "")
28
+ prompt += f"<|im_start|>{role}\n{content}<|im_end|>\n"
29
+
30
+ if prior_thinking:
31
+ prompt += f"<|im_start|>assistant\n<think>\n{prior_thinking}\n\n</think>\n"
32
+ elif think:
33
+ prompt += "<|im_start|>assistant\n<think>\n"
34
+ else:
35
+ prompt += "<|im_start|>assistant\n<think>\n\n</think>\n"
36
+ return prompt
37
+
38
+
39
+ class ExocoreRMSNorm(nn.Module):
40
+ def __init__(self, dim, eps=1e-6):
41
+ super().__init__()
42
+ self.eps = eps
43
+ self.weight = nn.Parameter(torch.ones(dim))
44
+
45
+ def forward(self, x):
46
+ xf = x.float()
47
+ norm = (xf.pow(2).mean(-1, keepdim=True) + self.eps).rsqrt()
48
+ return (xf * norm).to(x.dtype) * self.weight
49
+
50
+
51
+ class ExocoreRotaryEmbedding(nn.Module):
52
+ def __init__(self, dim, max_seq=40960, base=1000000):
53
+ super().__init__()
54
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
55
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
56
+ t = torch.arange(max_seq).float()
57
+ freqs = torch.outer(t, inv_freq)
58
+ cos = torch.cat([freqs.cos(), freqs.cos()], dim=-1)
59
+ sin = torch.cat([freqs.sin(), freqs.sin()], dim=-1)
60
+ self.register_buffer("rope_cos", cos, persistent=False)
61
+ self.register_buffer("rope_sin", sin, persistent=False)
62
+
63
+ def apply_rope(self, q, k):
64
+ T = q.shape[2]
65
+ cos = self.rope_cos[:T].unsqueeze(0).unsqueeze(0)
66
+ sin = self.rope_sin[:T].unsqueeze(0).unsqueeze(0)
67
+
68
+ def rotate_half(x):
69
+ half = x.shape[-1] // 2
70
+ return torch.cat([-x[..., half:], x[..., :half]], dim=-1)
71
+
72
+ q_rot = q * cos + rotate_half(q) * sin
73
+ k_rot = k * cos + rotate_half(k) * sin
74
+ return q_rot.to(q.dtype), k_rot.to(k.dtype)
75
+
76
+
77
+ class ExocoreAttention(nn.Module):
78
+ def __init__(self, cfg):
79
+ super().__init__()
80
+ self.n_heads = cfg["num_attention_heads"]
81
+ self.n_kv_heads = cfg["num_key_value_heads"]
82
+ self.head_dim = cfg["head_dim"]
83
+ hidden = cfg["hidden_size"]
84
+ eps = cfg["rms_norm_eps"]
85
+
86
+ self.q_proj = nn.Linear(hidden, self.n_heads * self.head_dim, bias=False)
87
+ self.k_proj = nn.Linear(hidden, self.n_kv_heads * self.head_dim, bias=False)
88
+ self.v_proj = nn.Linear(hidden, self.n_kv_heads * self.head_dim, bias=False)
89
+ self.o_proj = nn.Linear(self.n_heads * self.head_dim, hidden, bias=False)
90
+ self.q_norm = ExocoreRMSNorm(self.head_dim, eps=eps)
91
+ self.k_norm = ExocoreRMSNorm(self.head_dim, eps=eps)
92
+
93
+ def forward(self, x, rope, mask=None):
94
+ B, T, _ = x.shape
95
+ q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
96
+ k = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
97
+ v = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
98
+
99
+ q = self.q_norm(q)
100
+ k = self.k_norm(k)
101
+ q, k = rope.apply_rope(q, k)
102
+
103
+ groups = self.n_heads // self.n_kv_heads
104
+ k = k.repeat_interleave(groups, dim=1)
105
+ v = v.repeat_interleave(groups, dim=1)
106
+
107
+ scale = self.head_dim ** -0.5
108
+ attn = torch.matmul(q, k.transpose(-2, -1)) * scale
109
+ if mask is not None:
110
+ attn = attn + mask
111
+ attn = F.softmax(attn.float(), dim=-1).to(q.dtype)
112
+
113
+ out = torch.matmul(attn, v)
114
+ out = out.transpose(1, 2).contiguous().view(B, T, self.n_heads * self.head_dim)
115
+ return self.o_proj(out)
116
+
117
+
118
+ class ExocoreMLP(nn.Module):
119
+ def __init__(self, cfg):
120
+ super().__init__()
121
+ self.gate_proj = nn.Linear(cfg["hidden_size"], cfg["intermediate_size"], bias=False)
122
+ self.up_proj = nn.Linear(cfg["hidden_size"], cfg["intermediate_size"], bias=False)
123
+ self.down_proj = nn.Linear(cfg["intermediate_size"], cfg["hidden_size"], bias=False)
124
+
125
+ def forward(self, x):
126
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
127
+
128
+
129
+ class ExocoreBlock(nn.Module):
130
+ def __init__(self, cfg):
131
+ super().__init__()
132
+ self.input_layernorm = ExocoreRMSNorm(cfg["hidden_size"], eps=cfg["rms_norm_eps"])
133
+ self.post_attention_layernorm = ExocoreRMSNorm(cfg["hidden_size"], eps=cfg["rms_norm_eps"])
134
+ self.attn = ExocoreAttention(cfg)
135
+ self.mlp = ExocoreMLP(cfg)
136
+
137
+ def forward(self, x, rope, mask=None):
138
+ x = x + self.attn(self.input_layernorm(x), rope, mask)
139
+ x = x + self.mlp(self.post_attention_layernorm(x))
140
+ return x
141
+
142
+
143
+ class ExocoreLM(nn.Module):
144
+ def __init__(self, cfg):
145
+ super().__init__()
146
+ self.cfg = cfg
147
+ self.embed_tokens = nn.Embedding(cfg["vocab_size"], cfg["hidden_size"])
148
+ self.layers = nn.ModuleList([ExocoreBlock(cfg) for _ in range(cfg["num_hidden_layers"])])
149
+ self.norm = ExocoreRMSNorm(cfg["hidden_size"], eps=cfg["rms_norm_eps"])
150
+ self.lm_head = nn.Linear(cfg["hidden_size"], cfg["vocab_size"], bias=False)
151
+ self.rope = ExocoreRotaryEmbedding(
152
+ cfg["head_dim"],
153
+ max_seq=cfg.get("max_position_embeddings", 40960),
154
+ base=cfg.get("rope_theta", 1000000),
155
+ )
156
+
157
+ def forward(self, input_ids):
158
+ B, T = input_ids.shape
159
+ x = self.embed_tokens(input_ids)
160
+ mask = torch.triu(torch.full((T, T), float("-inf"), device=x.device), diagonal=1)
161
+ mask = mask.unsqueeze(0).unsqueeze(0)
162
+ for block in self.layers:
163
+ x = block(x, self.rope, mask)
164
+ x = self.norm(x)
165
+ return self.lm_head(x), None
main.py CHANGED
@@ -1,6 +1,8 @@
1
  import os
2
  import json
3
- import time
 
 
4
  from flask import Flask, render_template, request, jsonify, stream_with_context, Response
5
  try:
6
  from llama_cpp import Llama
@@ -10,15 +12,40 @@ except ImportError:
10
  LLAMA_AVAILABLE = False
11
  import trainer as tr
12
  import gemini_client as gc
 
13
  app = Flask(__name__)
14
 
15
- loaded_model = None
16
  loaded_model_path = None
17
- loaded_model_type = None # "gguf" | "pt"
18
- chat_history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- MODELS_DIR = os.path.join(os.path.dirname(__file__), "models")
21
- TRAINED_DIR = os.path.join(MODELS_DIR, "trained")
22
 
23
 
24
  def get_all_models():
@@ -31,11 +58,8 @@ def get_all_models():
31
  for f in sorted(os.listdir(TRAINED_DIR)):
32
  full = os.path.join(TRAINED_DIR, f)
33
  if f.endswith(".pt"):
34
- subtype = "qwen3" if tr.is_qwen3_pt(f) else None
35
- entry = {"name": f, "type": "pt"}
36
- if subtype:
37
- entry["subtype"] = subtype
38
- result.append(entry)
39
  elif os.path.isdir(full) and os.path.exists(os.path.join(full, "config.json")):
40
  result.append({"name": f, "type": "hf"})
41
  return result
@@ -51,53 +75,42 @@ def list_models():
51
  return jsonify({"models": get_all_models()})
52
 
53
 
 
 
 
 
 
54
  @app.route("/api/load", methods=["POST"])
55
  def load_model():
56
  global loaded_model, loaded_model_path, loaded_model_type, chat_history
57
-
58
- data = request.json
59
  model_name = data.get("model")
60
  model_type = data.get("type", "gguf")
61
-
62
  if not model_name:
63
  return jsonify({"error": "No model specified"}), 400
64
-
65
  try:
66
- if model_type == "hf":
67
- mc = tr.load_hf_model(model_name)
68
- loaded_model = mc
69
  loaded_model_path = model_name
70
- loaded_model_type = "hf"
71
  chat_history = []
72
- return jsonify({"success": True, "model": model_name, "type": "hf"})
73
  elif model_type == "pt":
74
- if tr.is_qwen3_pt(model_name):
75
- mc = tr.load_qwen3_model(model_name)
76
- loaded_model = mc
77
- loaded_model_path = model_name
78
- loaded_model_type = "pt"
79
- chat_history = []
80
- return jsonify({"success": True, "model": model_name, "type": "pt", "subtype": "qwen3"})
81
  mc = tr.load_trained_model(model_name)
82
- loaded_model = mc
83
  loaded_model_path = model_name
84
  loaded_model_type = "pt"
85
  chat_history = []
86
  return jsonify({"success": True, "model": model_name, "type": "pt"})
87
  else:
88
  if not LLAMA_AVAILABLE:
89
- return jsonify({"error": "llama-cpp-python is not installed. GGUF models are unavailable."}), 503
90
  n_ctx = int(data.get("n_ctx", 2048))
91
- n_threads = int(data.get("n_threads", 4))
92
  model_path = os.path.join(MODELS_DIR, model_name)
93
  if not os.path.exists(model_path):
94
  return jsonify({"error": f"Model not found: {model_name}"}), 404
95
- loaded_model = Llama(
96
- model_path=model_path,
97
- n_ctx=n_ctx,
98
- n_threads=n_threads,
99
- verbose=False
100
- )
101
  loaded_model_path = model_name
102
  loaded_model_type = "gguf"
103
  chat_history = []
@@ -106,116 +119,124 @@ def load_model():
106
  return jsonify({"error": str(e)}), 500
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  @app.route("/api/chat", methods=["POST"])
110
  def chat():
111
  global chat_history
112
-
113
  if loaded_model is None:
 
 
114
  return jsonify({"error": "No model loaded."}), 400
115
 
116
- data = request.json
117
  user_message = data.get("message", "").strip()
118
- max_tokens = int(data.get("max_tokens", 512))
119
- temperature = float(data.get("temperature", 0.7))
 
 
 
 
120
 
121
  if not user_message:
122
  return jsonify({"error": "Empty message"}), 400
123
 
124
- mode = data.get("mode", "fast")
125
- think_mode = (mode == "thinking")
126
-
127
- if loaded_model_type == "hf":
128
  messages = list(chat_history) + [{"role": "user", "content": user_message}]
129
- def generate_hf():
 
130
  full_reply = ""
131
- think_sent = False
132
  try:
133
- for event_type, payload in tr.generate_hf_stream(
134
- loaded_model_path, messages, max_tokens, temperature, think_mode
 
 
135
  ):
136
- if event_type == "think_block" and not think_sent:
137
  yield f"data: {json.dumps({'think_block': payload})}\n\n"
138
- think_sent = True
139
- elif event_type == "token":
140
  full_reply += payload
141
  yield f"data: {json.dumps({'token': payload})}\n\n"
142
- chat_history.append({"role": "user", "content": user_message})
143
  chat_history.append({"role": "assistant", "content": full_reply})
144
  yield f"data: {json.dumps({'done': True})}\n\n"
145
  except Exception as e:
146
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
147
- return Response(stream_with_context(generate_hf()), mimetype="text/event-stream")
 
148
 
149
  if loaded_model_type == "pt":
150
- if tr.is_qwen3_pt(loaded_model_path):
151
- messages = list(chat_history) + [{"role": "user", "content": user_message}]
152
- def generate_qwen3():
153
- full_reply = ""
154
- think_sent = False
155
- try:
156
- for event_type, payload in tr.generate_qwen3_stream(
157
- loaded_model_path, messages, max_tokens, temperature, think_mode
158
- ):
159
- if event_type == "think_block" and not think_sent:
160
- yield f"data: {json.dumps({'think_block': payload})}\n\n"
161
- think_sent = True
162
- elif event_type == "token":
163
- full_reply += payload
164
- yield f"data: {json.dumps({'token': payload})}\n\n"
165
- chat_history.append({"role": "user", "content": user_message})
166
- chat_history.append({"role": "assistant", "content": full_reply})
167
- yield f"data: {json.dumps({'done': True})}\n\n"
168
- except Exception as e:
169
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
170
- return Response(stream_with_context(generate_qwen3()), mimetype="text/event-stream")
171
-
172
- def generate_pt():
173
  try:
174
- result = tr.generate_text(
175
- loaded_model_path,
176
- user_message,
177
- max_tokens,
178
- temperature,
179
- think_mode=think_mode
180
- )
181
- think_block = result.get("think_block", "")
182
- reply = result["response"]
183
- if think_block:
184
- yield f"data: {json.dumps({'think_block': think_block})}\n\n"
185
- for ch in reply:
186
  yield f"data: {json.dumps({'token': ch})}\n\n"
187
- chat_history.append({"role": "user", "content": user_message})
188
- chat_history.append({"role": "assistant", "content": reply})
189
  yield f"data: {json.dumps({'done': True})}\n\n"
190
  except Exception as e:
191
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
192
- return Response(stream_with_context(generate_pt()), mimetype="text/event-stream")
193
 
194
- system_prompt = data.get("system_prompt", "You are exocoreAI, a helpful and knowledgeable assistant.")
195
  chat_history.append({"role": "user", "content": user_message})
196
- messages = [{"role": "system", "content": system_prompt}] + chat_history
197
 
198
- def generate_gguf():
199
- full_response = ""
200
  try:
201
- stream = loaded_model.create_chat_completion(
202
- messages=messages,
203
- max_tokens=max_tokens,
204
- temperature=temperature,
205
- stream=True
206
- )
207
  for chunk in stream:
208
  delta = chunk["choices"][0]["delta"]
209
  if "content" in delta:
210
- token = delta["content"]
211
- full_response += token
212
- yield f"data: {json.dumps({'token': token})}\n\n"
213
- chat_history.append({"role": "assistant", "content": full_response})
214
  yield f"data: {json.dumps({'done': True})}\n\n"
215
  except Exception as e:
216
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
217
-
218
- return Response(stream_with_context(generate_gguf()), mimetype="text/event-stream")
219
 
220
 
221
  @app.route("/api/clear", methods=["POST"])
@@ -228,13 +249,77 @@ def clear_history():
228
  @app.route("/api/status", methods=["GET"])
229
  def status():
230
  return jsonify({
231
- "model_loaded": loaded_model is not None,
232
- "model_name": loaded_model_path,
233
- "model_type": loaded_model_type,
234
- "history_length": len(chat_history)
 
235
  })
236
 
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  # ── Training routes ──────────────────────────────────────────────────────────
239
 
240
  @app.route("/train")
@@ -244,17 +329,14 @@ def train_page():
244
 
245
  @app.route("/api/train/start", methods=["POST"])
246
  def train_start():
247
- data = request.json
248
- text = data.get("text", "")
249
- config = data.get("config", {})
250
  model_name = data.get("model_name", "Exocore")
251
-
252
  if len(text) < 100:
253
  return jsonify({"error": "Training text too short (min 100 characters)"}), 400
254
-
255
  if tr.training_state["running"]:
256
  return jsonify({"error": "Training already in progress"}), 400
257
-
258
  tr.start_training(text, config, model_name)
259
  return jsonify({"success": True})
260
 
@@ -277,7 +359,6 @@ def train_models():
277
 
278
  @app.route("/api/train/model-info/<path:model_name>", methods=["GET"])
279
  def train_model_info(model_name):
280
- import torch
281
  save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
282
  path = os.path.join(save_dir, model_name)
283
  if not os.path.exists(path):
@@ -285,81 +366,35 @@ def train_model_info(model_name):
285
  try:
286
  file_size = os.path.getsize(path)
287
  modified = os.path.getmtime(path)
288
-
289
- # Qwen3-based model β€” read from meta file, don't try to load as MiniGPT
290
- if tr.is_qwen3_pt(model_name):
291
- import json as _j
292
- meta_path = os.path.join(save_dir, model_name + ".meta")
293
- meta = {}
294
- if os.path.exists(meta_path):
295
- with open(meta_path) as f:
296
- meta = _j.load(f)
297
- total_params = meta.get("params", 0)
298
  return jsonify({
299
- "name": model_name,
300
- "file_size_bytes": file_size,
301
- "file_size_mb": round(file_size / 1024 / 1024, 2),
302
- "modified": modified,
303
- "total_params": total_params,
304
- "trainable_params":total_params,
305
- "vocab_size": 151936,
306
- "n_embd": 1024,
307
- "n_layer": 28,
308
- "n_head": 16,
309
- "block_size": 32768,
310
- "dropout": 0,
311
- "layer_breakdown": {
312
- "embedding": 151936 * 1024,
313
- "attention": 28 * (1024 * 1024 * 3 + 1024 * 1024),
314
- "feedforward": 28 * (1024 * 2816 * 2 + 2816 * 1024),
315
- "layernorm": 28 * 2 * 1024 + 1024,
316
- "head": 151936 * 1024,
317
- },
318
- "vocab_chars": "Qwen3 BPE tokenizer (151,936 tokens)",
319
- "exocore_type": "qwen3",
320
  })
321
-
322
  ckpt = torch.load(path, map_location="cpu", weights_only=False)
323
  cfg = ckpt.get("config", {})
324
  stoi = ckpt.get("stoi", {})
325
- vocab_size = cfg.get("vocab_size", len(stoi))
326
- n_embd = cfg.get("n_embd", 0)
327
- n_layer = cfg.get("n_layer", 0)
328
- n_head = cfg.get("n_head", 0)
329
- block_size = cfg.get("block_size", 0)
330
- dropout = cfg.get("dropout", 0)
331
-
332
- from trainer import MiniGPT
333
- model = MiniGPT(vocab_size, block_size, n_embd, n_layer, n_head, dropout)
334
- model.load_state_dict(ckpt["model_state"])
335
- total_params = sum(p.numel() for p in model.parameters())
336
- trainable_params= sum(p.numel() for p in model.parameters() if p.requires_grad)
337
-
338
- layer_breakdown = {
339
- "embedding": vocab_size * n_embd + block_size * n_embd,
340
- "attention": n_layer * (3 * n_embd * n_embd + n_embd * n_embd),
341
- "feedforward":n_layer * (n_embd * 4 * n_embd + 4 * n_embd * n_embd),
342
- "layernorm": n_layer * (2 * n_embd * 2) + n_embd * 2,
343
- "head": n_embd * vocab_size,
344
- }
345
-
346
  return jsonify({
347
- "name": model_name,
348
- "file_size_bytes": file_size,
349
- "file_size_mb": round(file_size / 1024 / 1024, 2),
350
- "modified": modified,
351
- "total_params": total_params,
352
- "trainable_params":trainable_params,
353
- "vocab_size": vocab_size,
354
- "vocab_chars": sorted(stoi.keys()),
355
- "architecture": {
356
- "n_embd": n_embd,
357
- "n_layer": n_layer,
358
- "n_head": n_head,
359
- "block_size": block_size,
360
- "dropout": dropout,
361
- },
362
- "layer_breakdown": layer_breakdown,
363
  })
364
  except Exception as e:
365
  return jsonify({"error": str(e)}), 500
@@ -375,75 +410,28 @@ def get_train_text():
375
  return jsonify({"found": True, "text": text, "chars": len(text)})
376
 
377
 
378
- @app.route("/api/train/generate", methods=["POST"])
379
- def train_generate():
380
- data = request.json
381
- model_name = data.get("model_name")
382
- prompt = data.get("prompt", "")
383
- max_tokens = int(data.get("max_tokens", 200))
384
- temperature = float(data.get("temperature", 0.8))
385
-
386
- if not model_name:
387
- return jsonify({"error": "No model specified"}), 400
388
-
389
- try:
390
- result = tr.generate_text(model_name, prompt, max_tokens, temperature)
391
- return jsonify({"output": result["response"]})
392
- except Exception as e:
393
- return jsonify({"error": str(e)}), 500
394
-
395
-
396
- # ── HuggingFace download routes ───────────────────────────────────────────────
397
-
398
- @app.route("/api/convert/qwen", methods=["POST"])
399
- def convert_qwen():
400
- data = request.json or {}
401
- repo_id = data.get("repo_id", "Qwen/Qwen3-0.6B")
402
- out_name = data.get("out_name", "Exocore")
403
- if tr.get_convert_state()["running"]:
404
- return jsonify({"error": "Conversion already running"}), 400
405
- tr.start_qwen3_conversion(repo_id, out_name)
406
- return jsonify({"success": True})
407
-
408
-
409
- @app.route("/api/convert/status", methods=["GET"])
410
- def convert_status():
411
- return jsonify(tr.get_convert_state())
412
-
413
-
414
- @app.route("/api/hf/delete", methods=["POST"])
415
- def hf_delete():
416
  import shutil
417
- data = request.json or {}
418
  local_name = data.get("local_name", "")
419
  if not local_name:
420
  return jsonify({"error": "No model name given"}), 400
 
 
421
  path = os.path.join(TRAINED_DIR, local_name)
422
- if not os.path.isdir(path):
423
  return jsonify({"error": "Not found"}), 404
424
  try:
425
- shutil.rmtree(path)
426
- return jsonify({"success": True, "deleted": local_name})
 
 
 
427
  except Exception as e:
428
  return jsonify({"error": str(e)}), 500
429
 
430
 
431
- @app.route("/api/hf/download", methods=["POST"])
432
- def hf_download():
433
- data = request.json or {}
434
- repo_id = data.get("repo_id", "Qwen/Qwen3-0.6B")
435
- local_name = data.get("local_name", "Exocore")
436
- if tr.get_hf_download_state()["running"]:
437
- return jsonify({"error": "Download already running"}), 400
438
- tr.download_hf_model(repo_id, local_name)
439
- return jsonify({"success": True})
440
-
441
-
442
- @app.route("/api/hf/download/status", methods=["GET"])
443
- def hf_download_status():
444
- return jsonify(tr.get_hf_download_state())
445
-
446
-
447
  # ── Gemini routes ─────────────────────────────────────────────────────────────
448
 
449
  @app.route("/api/gemini/status", methods=["GET"])
@@ -481,15 +469,12 @@ def gemini_autotrain_start():
481
  data = request.json
482
  if gc.get_autotrain_status()["running"]:
483
  return jsonify({"error": "Auto-train already running"}), 400
484
-
485
  initial_prompt = data.get("initial_prompt", "").strip()
486
- continue_msg = data.get("continue_msg", "ok next train").strip()
487
- model_name = data.get("model_name", "Exocore").strip()
488
  config = data.get("config", {})
489
-
490
  if not initial_prompt:
491
  return jsonify({"error": "Initial prompt required"}), 400
492
-
493
  gc.start_autotrain(initial_prompt, continue_msg, model_name, config)
494
  return jsonify({"success": True})
495
 
@@ -507,15 +492,11 @@ def gemini_autotrain_status():
507
 
508
  SETTINGS_PATH = os.path.join(os.path.dirname(__file__), "settings.json")
509
  DEFAULT_SETTINGS = {
510
- "model_name": "Exocore",
511
- "epochs": 60,
512
- "n_embd": 384,
513
- "n_layer": 6,
514
- "block_size": 256,
515
- "batch_size": 16,
516
- "lr": 0.0002,
517
  }
518
 
 
519
  @app.route("/api/settings", methods=["GET"])
520
  def get_settings():
521
  if not os.path.exists(SETTINGS_PATH):
 
1
  import os
2
  import json
3
+ import threading
4
+ import requests as _requests
5
+ from bs4 import BeautifulSoup
6
  from flask import Flask, render_template, request, jsonify, stream_with_context, Response
7
  try:
8
  from llama_cpp import Llama
 
12
  LLAMA_AVAILABLE = False
13
  import trainer as tr
14
  import gemini_client as gc
15
+
16
  app = Flask(__name__)
17
 
18
+ loaded_model = None
19
  loaded_model_path = None
20
+ loaded_model_type = None
21
+ chat_history = []
22
+
23
+ MODELS_DIR = os.path.join(os.path.dirname(__file__), "models")
24
+ TRAINED_DIR = os.path.join(MODELS_DIR, "trained")
25
+ PROTECTED_MODELS = {"exocore.pt"}
26
+
27
+ _autoload_status = {"loading": False, "done": False, "error": None, "model": None}
28
+
29
+
30
+ def _autoload_exocore():
31
+ global loaded_model, loaded_model_path, loaded_model_type, _autoload_status
32
+ _autoload_status["loading"] = True
33
+ try:
34
+ pt_path = os.path.join(TRAINED_DIR, "Exocore.pt")
35
+ if os.path.exists(pt_path) and tr.is_exocore_pt("Exocore.pt"):
36
+ mc = tr.load_exocore_model("Exocore.pt")
37
+ loaded_model = mc
38
+ loaded_model_path = "Exocore.pt"
39
+ loaded_model_type = "exocore"
40
+ _autoload_status["done"] = True
41
+ _autoload_status["model"] = "Exocore.pt"
42
+ except Exception as e:
43
+ _autoload_status["error"] = str(e)
44
+ finally:
45
+ _autoload_status["loading"] = False
46
+
47
 
48
+ threading.Thread(target=_autoload_exocore, daemon=True).start()
 
49
 
50
 
51
  def get_all_models():
 
58
  for f in sorted(os.listdir(TRAINED_DIR)):
59
  full = os.path.join(TRAINED_DIR, f)
60
  if f.endswith(".pt"):
61
+ is_ex = tr.is_exocore_pt(f)
62
+ result.append({"name": f, "type": "exocore" if is_ex else "pt"})
 
 
 
63
  elif os.path.isdir(full) and os.path.exists(os.path.join(full, "config.json")):
64
  result.append({"name": f, "type": "hf"})
65
  return result
 
75
  return jsonify({"models": get_all_models()})
76
 
77
 
78
+ @app.route("/api/autoload-status", methods=["GET"])
79
+ def autoload_status():
80
+ return jsonify(_autoload_status)
81
+
82
+
83
  @app.route("/api/load", methods=["POST"])
84
  def load_model():
85
  global loaded_model, loaded_model_path, loaded_model_type, chat_history
86
+ data = request.json
 
87
  model_name = data.get("model")
88
  model_type = data.get("type", "gguf")
 
89
  if not model_name:
90
  return jsonify({"error": "No model specified"}), 400
 
91
  try:
92
+ if model_type == "exocore" or tr.is_exocore_pt(model_name):
93
+ mc = tr.load_exocore_model(model_name)
94
+ loaded_model = mc
95
  loaded_model_path = model_name
96
+ loaded_model_type = "exocore"
97
  chat_history = []
98
+ return jsonify({"success": True, "model": model_name, "type": "exocore"})
99
  elif model_type == "pt":
 
 
 
 
 
 
 
100
  mc = tr.load_trained_model(model_name)
101
+ loaded_model = mc
102
  loaded_model_path = model_name
103
  loaded_model_type = "pt"
104
  chat_history = []
105
  return jsonify({"success": True, "model": model_name, "type": "pt"})
106
  else:
107
  if not LLAMA_AVAILABLE:
108
+ return jsonify({"error": "llama-cpp-python not installed"}), 503
109
  n_ctx = int(data.get("n_ctx", 2048))
 
110
  model_path = os.path.join(MODELS_DIR, model_name)
111
  if not os.path.exists(model_path):
112
  return jsonify({"error": f"Model not found: {model_name}"}), 404
113
+ loaded_model = Llama(model_path=model_path, n_ctx=n_ctx, n_threads=4, verbose=False)
 
 
 
 
 
114
  loaded_model_path = model_name
115
  loaded_model_type = "gguf"
116
  chat_history = []
 
119
  return jsonify({"error": str(e)}), 500
120
 
121
 
122
+ @app.route("/api/model-details", methods=["GET"])
123
+ def model_details():
124
+ if loaded_model is None:
125
+ if _autoload_status["loading"]:
126
+ return jsonify({"loading": True})
127
+ return jsonify({"error": "No model loaded"}), 400
128
+ if loaded_model_type == "exocore":
129
+ cfg = loaded_model.get("config", {})
130
+ total = loaded_model.get("config", {})
131
+ params = sum(p.numel() for p in loaded_model["model"].parameters())
132
+ return jsonify({
133
+ "name": "ExocoreV1",
134
+ "type": "ExocoreV1",
135
+ "params": params,
136
+ "params_m": round(params / 1e6, 1),
137
+ "hidden_size": cfg.get("hidden_size", 1024),
138
+ "num_layers": cfg.get("num_hidden_layers", 28),
139
+ "num_heads": cfg.get("num_attention_heads", 16),
140
+ "num_kv_heads": cfg.get("num_key_value_heads", 8),
141
+ "head_dim": cfg.get("head_dim", 128),
142
+ "intermediate": cfg.get("intermediate_size", 3072),
143
+ "vocab_size": cfg.get("vocab_size", 151936),
144
+ "max_seq": cfg.get("max_position_embeddings", 40960),
145
+ "rope_theta": cfg.get("rope_theta", 1000000),
146
+ })
147
+ if loaded_model_type == "pt":
148
+ mc = loaded_model
149
+ params = sum(p.numel() for p in mc["model"].parameters())
150
+ cfg = mc["model"].ln_f.weight.shape
151
+ return jsonify({
152
+ "name": loaded_model_path,
153
+ "type": "MiniGPT",
154
+ "params": params,
155
+ "params_m": round(params / 1e6, 3),
156
+ "block_size": mc["block_size"],
157
+ "vocab_size": len(mc["stoi"]),
158
+ })
159
+ return jsonify({"name": loaded_model_path, "type": loaded_model_type})
160
+
161
+
162
  @app.route("/api/chat", methods=["POST"])
163
  def chat():
164
  global chat_history
 
165
  if loaded_model is None:
166
+ if _autoload_status["loading"]:
167
+ return jsonify({"error": "Model is still loading, please wait…"}), 503
168
  return jsonify({"error": "No model loaded."}), 400
169
 
170
+ data = request.json
171
  user_message = data.get("message", "").strip()
172
+ max_tokens = int(data.get("max_tokens", 512))
173
+ temperature = float(data.get("temperature", 0.7))
174
+ mode = data.get("mode", "fast")
175
+ deep_think = (mode == "deep")
176
+ think_mode = (mode == "thinking") or deep_think
177
+ search_ctx = data.get("search_context", None)
178
 
179
  if not user_message:
180
  return jsonify({"error": "Empty message"}), 400
181
 
182
+ if loaded_model_type == "exocore":
 
 
 
183
  messages = list(chat_history) + [{"role": "user", "content": user_message}]
184
+
185
+ def gen_exocore():
186
  full_reply = ""
 
187
  try:
188
+ for ev_type, payload in tr.generate_exocore_stream(
189
+ loaded_model_path, messages, max_tokens, temperature,
190
+ think_mode=think_mode, deep_think=deep_think,
191
+ search_context=search_ctx
192
  ):
193
+ if ev_type == "think_block":
194
  yield f"data: {json.dumps({'think_block': payload})}\n\n"
195
+ elif ev_type == "token":
 
196
  full_reply += payload
197
  yield f"data: {json.dumps({'token': payload})}\n\n"
198
+ chat_history.append({"role": "user", "content": user_message})
199
  chat_history.append({"role": "assistant", "content": full_reply})
200
  yield f"data: {json.dumps({'done': True})}\n\n"
201
  except Exception as e:
202
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
203
+
204
+ return Response(stream_with_context(gen_exocore()), mimetype="text/event-stream")
205
 
206
  if loaded_model_type == "pt":
207
+ def gen_pt():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  try:
209
+ result = tr.generate_text(loaded_model_path, user_message, max_tokens, temperature, think_mode=think_mode)
210
+ if result.get("think_block"):
211
+ yield f"data: {json.dumps({'think_block': result['think_block']})}\n\n"
212
+ for ch in result["response"]:
 
 
 
 
 
 
 
 
213
  yield f"data: {json.dumps({'token': ch})}\n\n"
214
+ chat_history.append({"role": "user", "content": user_message})
215
+ chat_history.append({"role": "assistant", "content": result["response"]})
216
  yield f"data: {json.dumps({'done': True})}\n\n"
217
  except Exception as e:
218
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
219
+ return Response(stream_with_context(gen_pt()), mimetype="text/event-stream")
220
 
 
221
  chat_history.append({"role": "user", "content": user_message})
222
+ messages_gguf = [{"role": "system", "content": "You are ExocoreV1, a helpful assistant."}] + chat_history
223
 
224
+ def gen_gguf():
225
+ full = ""
226
  try:
227
+ stream = loaded_model.create_chat_completion(messages=messages_gguf, max_tokens=max_tokens,
228
+ temperature=temperature, stream=True)
 
 
 
 
229
  for chunk in stream:
230
  delta = chunk["choices"][0]["delta"]
231
  if "content" in delta:
232
+ tok = delta["content"]
233
+ full += tok
234
+ yield f"data: {json.dumps({'token': tok})}\n\n"
235
+ chat_history.append({"role": "assistant", "content": full})
236
  yield f"data: {json.dumps({'done': True})}\n\n"
237
  except Exception as e:
238
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
239
+ return Response(stream_with_context(gen_gguf()), mimetype="text/event-stream")
 
240
 
241
 
242
  @app.route("/api/clear", methods=["POST"])
 
249
  @app.route("/api/status", methods=["GET"])
250
  def status():
251
  return jsonify({
252
+ "model_loaded": loaded_model is not None,
253
+ "model_name": loaded_model_path,
254
+ "model_type": loaded_model_type,
255
+ "history_length": len(chat_history),
256
+ "autoloading": _autoload_status["loading"],
257
  })
258
 
259
 
260
+ @app.route("/api/search", methods=["POST"])
261
+ def search():
262
+ data = request.json or {}
263
+ query = data.get("query", "").strip()
264
+ if not query:
265
+ return jsonify({"error": "No query"}), 400
266
+ try:
267
+ headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36"}
268
+ url = f"https://html.duckduckgo.com/html/?q={_requests.utils.quote(query)}"
269
+ resp = _requests.get(url, headers=headers, timeout=12)
270
+ soup = BeautifulSoup(resp.text, "html.parser")
271
+ results = []
272
+ for r in soup.select(".result")[:6]:
273
+ title_el = r.select_one(".result__title a")
274
+ snippet_el = r.select_one(".result__snippet")
275
+ url_el = r.select_one(".result__url")
276
+ href = title_el.get("href", "") if title_el else ""
277
+ if href.startswith("//duckduckgo.com/l/"):
278
+ import urllib.parse as _up
279
+ qs = _up.parse_qs(_up.urlparse(href).query)
280
+ href = qs.get("uddg", [href])[0]
281
+ if title_el:
282
+ results.append({
283
+ "title": title_el.get_text(strip=True),
284
+ "snippet": snippet_el.get_text(strip=True) if snippet_el else "",
285
+ "url": href or (url_el.get_text(strip=True) if url_el else ""),
286
+ })
287
+ return jsonify({"results": results, "query": query})
288
+ except Exception as e:
289
+ return jsonify({"error": str(e)}), 500
290
+
291
+
292
+ @app.route("/api/read-link", methods=["POST"])
293
+ def read_link():
294
+ data = request.json or {}
295
+ urls = data.get("urls", [])
296
+ if isinstance(urls, str):
297
+ urls = [urls]
298
+ if not urls:
299
+ url = data.get("url", "").strip()
300
+ if url:
301
+ urls = [url]
302
+ if not urls:
303
+ return jsonify({"error": "No URL provided"}), 400
304
+
305
+ headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36"}
306
+ pages = []
307
+ for url in urls[:4]:
308
+ try:
309
+ resp = _requests.get(url.strip(), headers=headers, timeout=12)
310
+ soup = BeautifulSoup(resp.text, "html.parser")
311
+ for tag in soup(["script", "style", "nav", "footer", "header", "aside", "noscript"]):
312
+ tag.decompose()
313
+ title = soup.title.get_text(strip=True) if soup.title else url
314
+ text = soup.get_text(separator="\n", strip=True)
315
+ text = "\n".join(ln for ln in text.splitlines() if ln.strip())[:4000]
316
+ pages.append({"url": url, "title": title, "text": text})
317
+ except Exception as e:
318
+ pages.append({"url": url, "error": str(e)})
319
+
320
+ return jsonify({"pages": pages})
321
+
322
+
323
  # ── Training routes ──────────────────────────────────────────────────────────
324
 
325
  @app.route("/train")
 
329
 
330
  @app.route("/api/train/start", methods=["POST"])
331
  def train_start():
332
+ data = request.json
333
+ text = data.get("text", "")
334
+ config = data.get("config", {})
335
  model_name = data.get("model_name", "Exocore")
 
336
  if len(text) < 100:
337
  return jsonify({"error": "Training text too short (min 100 characters)"}), 400
 
338
  if tr.training_state["running"]:
339
  return jsonify({"error": "Training already in progress"}), 400
 
340
  tr.start_training(text, config, model_name)
341
  return jsonify({"success": True})
342
 
 
359
 
360
  @app.route("/api/train/model-info/<path:model_name>", methods=["GET"])
361
  def train_model_info(model_name):
 
362
  save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
363
  path = os.path.join(save_dir, model_name)
364
  if not os.path.exists(path):
 
366
  try:
367
  file_size = os.path.getsize(path)
368
  modified = os.path.getmtime(path)
369
+ if tr.is_exocore_pt(model_name):
 
 
 
 
 
 
 
 
 
370
  return jsonify({
371
+ "name": model_name,
372
+ "type": "ExocoreV1",
373
+ "file_size_mb": round(file_size / 1024 / 1024, 2),
374
+ "modified": modified,
375
+ "total_params": 751632384,
376
+ "params_m": 751.6,
377
+ "hidden_size": 1024,
378
+ "num_layers": 28,
379
+ "num_heads": 16,
380
+ "num_kv_heads": 8,
381
+ "head_dim": 128,
382
+ "intermediate": 3072,
383
+ "vocab_size": 151936,
 
 
 
 
 
 
 
 
384
  })
 
385
  ckpt = torch.load(path, map_location="cpu", weights_only=False)
386
  cfg = ckpt.get("config", {})
387
  stoi = ckpt.get("stoi", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  return jsonify({
389
+ "name": model_name,
390
+ "type": "MiniGPT",
391
+ "file_size_mb": round(file_size / 1024 / 1024, 2),
392
+ "modified": modified,
393
+ "vocab_size": cfg.get("vocab_size", len(stoi)),
394
+ "n_embd": cfg.get("n_embd", 0),
395
+ "n_layer": cfg.get("n_layer", 0),
396
+ "n_head": cfg.get("n_head", 0),
397
+ "block_size": cfg.get("block_size", 0),
 
 
 
 
 
 
 
398
  })
399
  except Exception as e:
400
  return jsonify({"error": str(e)}), 500
 
410
  return jsonify({"found": True, "text": text, "chars": len(text)})
411
 
412
 
413
+ @app.route("/api/model/delete", methods=["POST"])
414
+ def model_delete():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  import shutil
416
+ data = request.json or {}
417
  local_name = data.get("local_name", "")
418
  if not local_name:
419
  return jsonify({"error": "No model name given"}), 400
420
+ if local_name.lower() in PROTECTED_MODELS:
421
+ return jsonify({"error": "This model is protected and cannot be deleted."}), 403
422
  path = os.path.join(TRAINED_DIR, local_name)
423
+ if not os.path.exists(path):
424
  return jsonify({"error": "Not found"}), 404
425
  try:
426
+ if os.path.isdir(path):
427
+ shutil.rmtree(path)
428
+ else:
429
+ os.remove(path)
430
+ return jsonify({"success": True})
431
  except Exception as e:
432
  return jsonify({"error": str(e)}), 500
433
 
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  # ── Gemini routes ─────────────────────────────────────────────────────────────
436
 
437
  @app.route("/api/gemini/status", methods=["GET"])
 
469
  data = request.json
470
  if gc.get_autotrain_status()["running"]:
471
  return jsonify({"error": "Auto-train already running"}), 400
 
472
  initial_prompt = data.get("initial_prompt", "").strip()
473
+ continue_msg = data.get("continue_msg", "ok next train").strip()
474
+ model_name = data.get("model_name", "Exocore").strip()
475
  config = data.get("config", {})
 
476
  if not initial_prompt:
477
  return jsonify({"error": "Initial prompt required"}), 400
 
478
  gc.start_autotrain(initial_prompt, continue_msg, model_name, config)
479
  return jsonify({"success": True})
480
 
 
492
 
493
  SETTINGS_PATH = os.path.join(os.path.dirname(__file__), "settings.json")
494
  DEFAULT_SETTINGS = {
495
+ "model_name": "Exocore", "epochs": 60, "n_embd": 384,
496
+ "n_layer": 6, "block_size": 256, "batch_size": 16, "lr": 0.0002,
 
 
 
 
 
497
  }
498
 
499
+
500
  @app.route("/api/settings", methods=["GET"])
501
  def get_settings():
502
  if not os.path.exists(SETTINGS_PATH):
replit.md CHANGED
@@ -1,91 +1,91 @@
1
- # exocoreAI
2
-
3
- A local AI chat interface β€” goal is to build **Exocore.pt**, a fully custom-trained AI model owned by the user, with HuggingFace (Qwen3-0.6B) as a teacher/reference brain.
4
-
5
- ## Big Picture Goal
6
-
7
- Make **Exocore.pt** the real thing β€” a proper large model (~1.5GB, word-level transformer, not character-level) trained on custom data, distilled/guided by Qwen3-0.6B. The HF model is temporary scaffolding. Final state: one Exocore.pt that replaces HF entirely.
8
-
9
- ### Roadmap (continue next session)
10
- 1. **Convert Qwen3 β†’ Exocore.pt** β€” DONE βœ“. `models/trained/Exocore.pt` is now 1.5 GB, 751.6M params, full Qwen3-0.6B weights. No transformers needed β€” pure PyTorch.
11
- 2. **Knowledge distillation loop** β€” use Exocore.pt to generate training data, fine-tune on those outputs
12
- 3. **Multi-turn chat fine-tuning** β€” add LoRA-style fine-tuning on top of the converted Qwen3 Exocore.pt using custom data
13
-
14
- ## Current State (session ended)
15
-
16
- ### Models on disk
17
- - `models/trained/Exocore.pt` β€” **1.5 GB, 751.6M params, Qwen3-0.6B architecture** (28 layers, 1024 hidden, 16 heads, 8 KV heads, 151936 vocab, bfloat16)
18
- - `models/trained/Exocore.pt.meta` β€” tiny JSON file `{"exocore_type": "qwen3", "params": 751632384}` for instant type detection
19
-
20
- ### Key architecture files
21
- - `qwen3_model.py` β€” pure PyTorch Qwen3 (RMSNorm, RoPE, GQA, SwiGLU). `apply_rope()` casts cos/sin to q.dtype (bfloat16 fix).
22
- - `trainer.py`: `load_qwen3_model()` creates model in bfloat16 from start (avoids memory doubling), deletes checkpoint after loading. `is_qwen3_pt()` uses .meta file (instant, no file load).
23
- - Conversion pipeline: `start_qwen3_conversion()` saves .pt + .meta file
24
- - API routes: `POST /api/convert/qwen`, `GET /api/convert/status`
25
-
26
- ### UI state
27
- - Large qwen3 .pt models are NOT auto-loaded on page open (require explicit click in Settings β€” prevents OOM on low-RAM machines)
28
- - When qwen3 .pt is loaded: shows "Chat with Exocore..." placeholder, "EXOCORE" pill label, no "Text Continuation Mode" banner
29
- - `applyModelLoaded(name, type, subtype)` β€” subtype="qwen3" routes UI to chat mode
30
- - `/api/models` returns `{"name": "Exocore.pt", "type": "pt", "subtype": "qwen3"}`
31
- - `/api/load` returns `{"success": true, "subtype": "qwen3"}` for qwen3 models
32
-
33
- ### Training config (settings.json)
34
- ```json
35
- {"epochs":60,"n_embd":384,"n_layer":6,"n_head":6,"block_size":256,"batch_size":16,"lr":0.0002}
36
- ```
37
- - Training was running (60 epochs, 384 embd, 6 layers, 6 heads, 256 ctx) β€” **restart training after every server restart** via `/api/train/start`
38
- - Training data: `train.txt` (60K chars, includes `<think>...</think>` formatted Q&A examples)
39
-
40
- ### HF model integration (already done)
41
- - `trainer.py` has: `load_hf_model()`, `generate_hf_text()`, `generate_hf_stream()`, `download_hf_model()`, `is_hf_model()`, `get_hf_download_state()`
42
- - `main.py` has: `/api/hf/download`, `/api/hf/download/status`, HF model detection (dir with config.json = HF type)
43
- - `index.html` has: gold HF type pill, think-block green panel, Fast/Thinking mode buttons, auto-prefer HF Exocore
44
-
45
- ### API routes
46
- - `GET /` β€” Chat UI
47
- - `GET /train` β€” Training UI
48
- - `GET /api/models` β€” List all models (hf + pt)
49
- - `POST /api/load` β€” Load model `{ model, type }`
50
- - `POST /api/chat` β€” SSE streaming chat
51
- - `POST /api/clear` β€” Clear history
52
- - `GET /api/status` β€” Model status
53
- - `GET /api/settings` / `POST /api/settings` β€” Persist settings
54
- - `GET /api/train/text` β€” Get train.txt
55
- - `POST /api/train/start` β€” Start training
56
- - `GET /api/train/status` β€” Live training state
57
- - `POST /api/train/stop` β€” Stop training
58
- - `GET /api/train/models` β€” List .pt models
59
- - `POST /api/train/generate` β€” Test generation
60
- - `POST /api/hf/download` β€” Download HF model
61
- - `GET /api/hf/download/status` β€” HF download progress
62
-
63
- ## Project structure
64
-
65
- - `main.py` β€” Flask server and all API routes
66
- - `trainer.py` β€” MiniGPT + HF model support
67
- - `templates/index.html` β€” Chat UI (Fast/Thinking buttons, think-block panel, HF model in select)
68
- - `templates/train.html` β€” Training UI with live loss chart and auto-train
69
- - `gemini_client.py` β€” Gemini auto-train integration
70
- - `train.txt` β€” Training data (60K chars, never overwrite β€” always append)
71
- - `settings.json` β€” Persisted training config
72
- - `models/` β€” GGUF files go here
73
- - `models/trained/` β€” .pt files and HF dirs go here
74
-
75
- ## Generation modes
76
-
77
- - **Thinking** β€” temp 0.3, 300 tokens β€” coherent, uses `<think>` block
78
- - **Fast** β€” temp 0.9, 80 tokens β€” quick, varied
79
-
80
- ## User preferences
81
-
82
- - Project named: exocoreAI
83
- - Model name: always **Exocore** (Exocore.pt or Exocore/ dir)
84
- - Goal: build own AI β€” Exocore.pt should eventually replace HF model entirely
85
- - HF model (Qwen3-0.6B) = teacher brain, not the end goal
86
- - Next big step: knowledge distillation HF→PT + scale up MiniGPT architecture
87
- - Training data in train.txt β€” always append, never overwrite
88
- - Generation modes: Thinking (temp 0.3) and Fast (temp 0.9)
89
- - Settings persist in settings.json
90
- - Language goal: proper English first, multi-language later
91
- - llama-cpp-python NOT installed (guarded with try/except + LLAMA_AVAILABLE flag)
 
1
+ # ExocoreV1
2
+
3
+ A from-scratch AI system built around **ExocoreV1** β€” a powerful 751M-parameter language model, fully rebranded and independent of any third-party naming.
4
+
5
+ **HuggingFace:** https://huggingface.co/Exocore/ExocoreV1
6
+
7
+ ## Project Overview
8
+
9
+ ExocoreV1 is a full-stack AI chat and training platform:
10
+
11
+ - **Chat interface** β€” Talk to ExocoreV1 with Fast, Thinking, and Deep Think modes
12
+ - **Research tools** β€” Web search (DuckDuckGo) + multi-URL reader integrated into the chat pipeline
13
+ - **Training page** β€” Train new models from text, with live Chart.js loss curve and architecture tree visualization
14
+ - **Auto-train** β€” Gemini-powered automatic data generation + training
15
+ - **Auto-load** β€” Exocore.pt loads automatically on startup (no button click needed)
16
+
17
+ ## Architecture
18
+
19
+ - **Backend**: Python + Flask (`main.py`)
20
+ - **Model engine**: `exocore_model.py` β€” ExocoreLM (28-layer transformer, 751M params, GQA, RoPE, SwiGLU)
21
+ - **Training**: `trainer.py` β€” MiniGPT character-level trainer + ExocoreV1 inference
22
+ - **Gemini client**: `gemini_client.py`
23
+ - **Templates**: `templates/index.html`, `templates/train.html`
24
+
25
+ ## Key Files
26
+
27
+ | File | Purpose |
28
+ |------|---------|
29
+ | `main.py` | Flask server, all API routes |
30
+ | `exocore_model.py` | ExocoreLM architecture (Qwen3-compatible, ExocoreV1 branded) |
31
+ | `trainer.py` | Training loop + MiniGPT + ExocoreV1 inference |
32
+ | `gemini_client.py` | Gemini API integration |
33
+ | `models/trained/Exocore.pt` | ExocoreV1 model weights (751M params, PROTECTED) |
34
+
35
+ ## ExocoreV1 Model Architecture
36
+
37
+ | Component | Value |
38
+ |-----------|-------|
39
+ | Parameters | 751.6M |
40
+ | Layers | 28 |
41
+ | Hidden size | 1024 |
42
+ | Attention heads | 16Q / 8 KV |
43
+ | Head dim | 128 |
44
+ | FFN intermediate | 3072 (SwiGLU) |
45
+ | Vocab size | 151,936 |
46
+ | Max context | 40,960 tokens |
47
+ | RoPE theta | 1,000,000 |
48
+ | Precision | bfloat16 |
49
+
50
+ ## API Endpoints
51
+
52
+ | Endpoint | Method | Description |
53
+ |----------|--------|-------------|
54
+ | `/api/models` | GET | List all available models |
55
+ | `/api/load` | POST | Load a model |
56
+ | `/api/chat` | POST | Stream chat (SSE) |
57
+ | `/api/clear` | POST | Clear chat history |
58
+ | `/api/status` | GET | Server/model status |
59
+ | `/api/autoload-status` | GET | Auto-load progress |
60
+ | `/api/model-details` | GET | Current model architecture info |
61
+ | `/api/search` | POST | DuckDuckGo web search |
62
+ | `/api/read-link` | POST | Fetch and parse URL content (multi-link) |
63
+ | `/api/train/start` | POST | Start training |
64
+ | `/api/train/status` | GET | Training status + loss history |
65
+ | `/api/train/stop` | POST | Stop training |
66
+ | `/api/train/models` | GET | List trained models |
67
+ | `/api/train/model-info/<name>` | GET | Model architecture info |
68
+ | `/api/gemini/*` | POST/GET | Gemini integration |
69
+
70
+ ## Chat Modes
71
+
72
+ | Mode | Description |
73
+ |------|-------------|
74
+ | Fast | Direct answer, no thinking |
75
+ | Thinking | Single-pass reasoning before answering |
76
+ | Deep Think | Two-pass multi-step reasoning (Pass 1 + Pass 2 think blocks) |
77
+
78
+ ## Research Features
79
+
80
+ - **Web search**: DuckDuckGo HTML scraping, returns title + snippet + URL
81
+ - **Read Page**: Fetches any URL, strips nav/scripts, returns clean text (up to 4000 chars)
82
+ - **Multi-link reading**: Read up to 4 URLs simultaneously and merge into research context
83
+ - **Context injection**: Research context is automatically injected into the model prompt
84
+
85
+ ## User Preferences
86
+
87
+ - No system prompt exposed in UI β€” ExocoreV1 identity is baked into the model prompt automatically
88
+ - Exocore.pt is permanently protected from deletion
89
+ - No "qwen" branding anywhere in the UI or code
90
+ - Model auto-loads on startup
91
+ - No system prompt field in UI β€” identity lives inside the model
templates/index.html ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ExocoreV1</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: 'Segoe UI', sans-serif; background: #0d0d0f; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
10
+ header { background: #111116; border-bottom: 1px solid #1e1e2a; padding: 10px 20px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
11
+ .logo { display: flex; align-items: center; gap: 8px; color: #a78bfa; font-weight: 700; font-size: 1rem; letter-spacing: 1px; }
12
+ header nav a { color: #666; text-decoration: none; margin-left: 14px; font-size: 0.83rem; }
13
+ header nav a:hover { color: #a78bfa; }
14
+ .main { display: flex; flex: 1; overflow: hidden; }
15
+ .sidebar { width: 256px; background: #111116; border-right: 1px solid #1e1e2a; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
16
+ .section-label { font-size: 0.67rem; color: #444; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 5px; }
17
+ .model-card { background: #16161f; border: 1px solid #252535; border-radius: 7px; padding: 9px 11px; cursor: pointer; transition: border-color .15s; margin-bottom: 5px; }
18
+ .model-card:hover { border-color: #7c3aed; }
19
+ .model-card.selected { border-color: #a78bfa; background: #1a1628; }
20
+ .model-card .mn { font-size: 0.82rem; color: #ddd; font-weight: 500; }
21
+ .model-card .mt { font-size: 0.7rem; color: #555; margin-top: 2px; }
22
+ .pb { font-size: 0.63rem; padding: 1px 5px; border-radius: 20px; background: #1a1628; border: 1px solid #4c1d95; color: #a78bfa; margin-left: 4px; }
23
+ .status-pill { display: inline-flex; align-items: center; gap: 5px; background: #16161f; border: 1px solid #252535; border-radius: 20px; padding: 4px 10px; font-size: 0.73rem; color: #555; }
24
+ .status-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: #333; flex-shrink: 0; }
25
+ .status-pill .dot.loading { background: #f59e0b; animation: pulse 1s infinite; }
26
+ .status-pill .dot.ready { background: #22c55e; }
27
+ .status-pill .dot.off { background: #333; }
28
+ @keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} }
29
+ .btn { border: none; border-radius: 6px; padding: 8px 14px; cursor: pointer; font-size: 0.8rem; font-weight: 500; }
30
+ .btn-p { background: #7c3aed; color: white; width: 100%; }
31
+ .btn-p:hover { background: #6d28d9; }
32
+ .btn-p:disabled { background: #252535; color: #555; cursor: not-allowed; }
33
+ .param-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
34
+ .param-row label { font-size: 0.73rem; color: #555; }
35
+ .param-row input { background: #16161f; border: 1px solid #252535; color: #ddd; border-radius: 5px; padding: 4px 8px; font-size: 0.78rem; width: 90px; }
36
+ .param-row input:focus { outline: none; border-color: #7c3aed; }
37
+ .details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
38
+ .det-cell { background: #0d0d10; border: 1px solid #1a1a24; border-radius: 5px; padding: 5px 7px; }
39
+ .det-cell .dv { font-size: 0.8rem; color: #c4b5fd; font-weight: 600; }
40
+ .det-cell .dk { font-size: 0.63rem; color: #444; margin-top: 1px; }
41
+ .gemini-dot { width: 7px; height: 7px; border-radius: 50%; background: #333; display: inline-block; margin-right: 4px; }
42
+ .gemini-dot.on { background: #22c55e; }
43
+ .chat-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
44
+ .messages { flex: 1; overflow-y: auto; padding: 24px 32px; display: flex; flex-direction: column; gap: 18px; }
45
+ .welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 10px; }
46
+ .welcome h2 { font-size: 1.6rem; color: #a78bfa; font-weight: 700; }
47
+ .welcome p { color: #444; font-size: 0.9rem; }
48
+ .msg { max-width: 800px; }
49
+ .msg.user { align-self: flex-end; }
50
+ .msg.assistant { align-self: flex-start; width: 100%; max-width: 800px; }
51
+ .msg-bubble { padding: 11px 15px; border-radius: 10px; font-size: 0.88rem; line-height: 1.65; white-space: pre-wrap; word-wrap: break-word; }
52
+ .msg.user .msg-bubble { background: #1a1628; border: 1px solid #4c1d95; color: #e2e8f0; max-width: 600px; }
53
+ .msg.assistant .msg-bubble { background: #111116; border: 1px solid #1e1e2a; color: #ddd; }
54
+ .think-block { background: #080d14; border: 1px solid #1e2f4a; border-radius: 8px; padding: 10px 14px; margin-bottom: 8px; }
55
+ .think-header { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; margin-bottom: 5px; }
56
+ .think-header .th-label { font-size: 0.72rem; color: #374151; text-transform: uppercase; letter-spacing: 0.5px; }
57
+ .think-badge { font-size: 0.63rem; background: #0d1f36; border: 1px solid #1e3a5f; color: #3b82f6; padding: 1px 6px; border-radius: 20px; }
58
+ .think-toggle { margin-left: auto; font-size: 0.68rem; color: #374151; }
59
+ .think-content { font-size: 0.78rem; color: #4b5563; white-space: pre-wrap; line-height: 1.5; overflow: hidden; transition: max-height 0.3s ease; }
60
+ .think-content.collapsed { max-height: 0 !important; }
61
+ .ctx-row { padding: 4px 20px 0; display: none; }
62
+ .ctx-badge { display: inline-flex; align-items: center; gap: 4px; background: #0d1f36; border: 1px solid #1e3a5f; border-radius: 20px; padding: 3px 10px; font-size: 0.7rem; color: #3b82f6; }
63
+ .ctx-badge button { background: none; border: none; color: #3b82f6; cursor: pointer; font-size: 0.8rem; padding: 0 0 0 4px; line-height: 1; }
64
+ .search-panel { border-top: 1px solid #1e1e2a; background: #0d0d10; padding: 8px 16px; display: none; align-items: center; gap: 8px; }
65
+ .search-panel.open { display: flex; }
66
+ .search-input { flex: 1; background: #16161f; border: 1px solid #252535; color: #ddd; border-radius: 6px; padding: 7px 12px; font-size: 0.82rem; font-family: inherit; }
67
+ .search-input:focus { outline: none; border-color: #3b82f6; }
68
+ .search-results { background: #0d0d10; border-top: 1px solid #1a1a24; padding: 0 16px; display: none; max-height: 200px; overflow-y: auto; }
69
+ .search-results.open { display: block; padding: 8px 16px; }
70
+ .sr-item { background: #111116; border: 1px solid #1e1e2a; border-radius: 6px; padding: 8px 11px; margin-bottom: 6px; }
71
+ .sr-item .sr-title { font-size: 0.8rem; color: #93c5fd; font-weight: 500; }
72
+ .sr-item .sr-snip { font-size: 0.73rem; color: #555; margin-top: 2px; line-height: 1.4; }
73
+ .sr-item .sr-url { font-size: 0.67rem; color: #374151; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
74
+ .sr-item .sr-actions { display: flex; gap: 5px; margin-top: 6px; }
75
+ .sr-btn { font-size: 0.68rem; padding: 3px 8px; border-radius: 4px; border: 1px solid #252535; background: #16161f; color: #666; cursor: pointer; }
76
+ .sr-btn:hover { border-color: #3b82f6; color: #3b82f6; }
77
+ .sr-msg { font-size: 0.75rem; color: #555; padding: 6px 0; }
78
+ .bottom-bar { border-top: 1px solid #1e1e2a; background: #111116; padding: 10px 20px; flex-shrink: 0; }
79
+ .mode-row { display: flex; align-items: center; gap: 5px; margin-bottom: 8px; flex-wrap: wrap; }
80
+ .mode-btn { background: #16161f; border: 1px solid #252535; color: #666; padding: 5px 13px; border-radius: 6px; cursor: pointer; font-size: 0.78rem; }
81
+ .mode-btn.active { background: #1a1628; border-color: #a78bfa; color: #a78bfa; }
82
+ .mode-btn.deep.active { background: #0d1f36; border-color: #3b82f6; color: #3b82f6; }
83
+ .mode-btn:hover:not(.active) { border-color: #444; color: #aaa; }
84
+ .tool-btn { background: #16161f; border: 1px solid #252535; color: #666; padding: 5px 11px; border-radius: 6px; cursor: pointer; font-size: 0.78rem; margin-left: auto; display: flex; align-items: center; gap: 4px; }
85
+ .tool-btn:hover { border-color: #555; color: #aaa; }
86
+ .tool-btn.active { border-color: #3b82f6; color: #3b82f6; background: #0d1f36; }
87
+ .input-row { display: flex; gap: 8px; align-items: flex-end; }
88
+ .msg-input { flex: 1; background: #16161f; border: 1px solid #252535; color: #ddd; border-radius: 8px; padding: 10px 14px; font-size: 0.88rem; resize: none; max-height: 120px; font-family: inherit; line-height: 1.4; }
89
+ .msg-input:focus { outline: none; border-color: #7c3aed; }
90
+ .send-btn { background: #7c3aed; color: white; border: none; border-radius: 8px; padding: 10px 18px; cursor: pointer; font-size: 0.88rem; font-weight: 500; height: 42px; }
91
+ .send-btn:hover { background: #6d28d9; }
92
+ .send-btn:disabled { background: #252535; color: #555; cursor: not-allowed; }
93
+ ::-webkit-scrollbar { width: 5px; }
94
+ ::-webkit-scrollbar-thumb { background: #2a2a3a; border-radius: 3px; }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <header>
99
+ <div class="logo">
100
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"/></svg>
101
+ ExocoreV1
102
+ </div>
103
+ <nav><a href="/">Chat</a><a href="/train">Train</a></nav>
104
+ </header>
105
+
106
+ <div class="main">
107
+ <!-- SIDEBAR -->
108
+ <div class="sidebar">
109
+ <div>
110
+ <div class="section-label">Models</div>
111
+ <div id="modelList"><p style="color:#333;font-size:0.75rem;">Loading…</p></div>
112
+ </div>
113
+ <div>
114
+ <div class="section-label">Status</div>
115
+ <div class="status-pill"><span class="dot loading" id="statusDot"></span><span id="statusText">Auto-loading…</span></div>
116
+ </div>
117
+ <div id="loadBtnWrap" style="display:none;">
118
+ <button class="btn btn-p" id="loadBtn" onclick="loadSelectedModel()">Load Selected Model</button>
119
+ </div>
120
+
121
+ <div>
122
+ <div class="section-label">Parameters</div>
123
+ <div class="param-row"><label>Max Tokens</label><input type="number" id="maxTokens" value="512" min="64" max="4096"></div>
124
+ <div class="param-row"><label>Temperature</label><input type="number" id="temperature" value="0.7" step="0.05" min="0.1" max="2"></div>
125
+ </div>
126
+
127
+ <div id="detailsSection" style="display:none;">
128
+ <div class="section-label">Model Details</div>
129
+ <div class="details-grid" id="detailsGrid"></div>
130
+ </div>
131
+
132
+ <div>
133
+ <div class="section-label">Gemini</div>
134
+ <div class="param-row" style="margin-bottom:0;">
135
+ <span style="font-size:0.75rem;color:#555;"><span class="gemini-dot" id="geminiDot"></span><span id="geminiStatus">Not initialized</span></span>
136
+ <button onclick="initGemini()" style="background:#16161f;border:1px solid #252535;color:#666;padding:3px 9px;font-size:0.72rem;border-radius:5px;cursor:pointer;">Init</button>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- CHAT -->
142
+ <div class="chat-area">
143
+ <div class="messages" id="messages">
144
+ <div class="welcome" id="welcome">
145
+ <h2>ExocoreV1</h2>
146
+ <p id="welcomeSub">Auto-loading model…</p>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="ctx-row" id="ctxRow">
151
+ <span class="ctx-badge">
152
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
153
+ <span id="ctxLabel">Research context active</span>
154
+ <button onclick="clearContext()">Γ—</button>
155
+ </span>
156
+ </div>
157
+
158
+ <div class="search-panel" id="searchPanel">
159
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
160
+ <input type="text" class="search-input" id="searchInput" placeholder="Search the web for research…" onkeydown="if(event.key==='Enter')runSearch()">
161
+ <button onclick="runSearch()" style="background:#1e3a5f;border:1px solid #1e4a7f;color:#93c5fd;padding:6px 12px;border-radius:6px;font-size:0.78rem;cursor:pointer;">Search</button>
162
+ <button onclick="closeSearch()" style="background:#16161f;border:1px solid #252535;color:#555;padding:6px 10px;border-radius:6px;font-size:0.78rem;cursor:pointer;">βœ•</button>
163
+ </div>
164
+ <div class="search-results" id="searchResults"></div>
165
+
166
+ <div class="bottom-bar">
167
+ <div class="mode-row">
168
+ <button class="mode-btn active" id="mFast" onclick="setMode('fast')">Fast</button>
169
+ <button class="mode-btn" id="mThink" onclick="setMode('thinking')">Thinking</button>
170
+ <button class="mode-btn deep" id="mDeep" onclick="setMode('deep')">Deep Think</button>
171
+ <button class="mode-btn" onclick="clearChat()">Clear Chat</button>
172
+ <button class="tool-btn" id="researchBtn" onclick="toggleSearch()">
173
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
174
+ Research
175
+ </button>
176
+ </div>
177
+ <div class="input-row">
178
+ <textarea class="msg-input" id="msgInput" rows="1"
179
+ placeholder="Type a message… (Enter to send, Shift+Enter for newline)"
180
+ onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
181
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">Send</button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <script>
188
+ let selectedModel = null;
189
+ let currentMode = 'fast';
190
+ let searchContext = null;
191
+ let isGenerating = false;
192
+ let loadedModel = null;
193
+
194
+ function escHtml(s) {
195
+ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
196
+ }
197
+ function autoResize(el) {
198
+ el.style.height = 'auto';
199
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
200
+ }
201
+ function handleKey(e) {
202
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
203
+ }
204
+
205
+ function setMode(m) {
206
+ currentMode = m;
207
+ document.getElementById('mFast').classList.toggle('active', m === 'fast');
208
+ document.getElementById('mThink').classList.toggle('active', m === 'thinking');
209
+ document.getElementById('mDeep').classList.toggle('active', m === 'deep');
210
+ }
211
+
212
+ function toggleSearch() {
213
+ const p = document.getElementById('searchPanel');
214
+ const b = document.getElementById('researchBtn');
215
+ const open = p.classList.contains('open');
216
+ if (open) { closeSearch(); }
217
+ else { p.classList.add('open'); b.classList.add('active'); document.getElementById('searchInput').focus(); }
218
+ }
219
+ function closeSearch() {
220
+ document.getElementById('searchPanel').classList.remove('open');
221
+ document.getElementById('searchResults').classList.remove('open');
222
+ document.getElementById('researchBtn').classList.remove('active');
223
+ }
224
+ function clearContext() {
225
+ searchContext = null;
226
+ document.getElementById('ctxRow').style.display = 'none';
227
+ }
228
+ function setContext(text) {
229
+ if (searchContext) searchContext += '\n\n' + text;
230
+ else searchContext = text;
231
+ document.getElementById('ctxRow').style.display = 'block';
232
+ document.getElementById('ctxLabel').textContent = 'Research context (' + searchContext.length + ' chars)';
233
+ }
234
+
235
+ async function runSearch() {
236
+ const q = document.getElementById('searchInput').value.trim();
237
+ if (!q) return;
238
+ const sr = document.getElementById('searchResults');
239
+ sr.innerHTML = '<div class="sr-msg">Searching…</div>';
240
+ sr.classList.add('open');
241
+ try {
242
+ const r = await fetch('/api/search', {
243
+ method:'POST', headers:{'Content-Type':'application/json'},
244
+ body: JSON.stringify({ query: q })
245
+ });
246
+ const d = await r.json();
247
+ if (d.error) { sr.innerHTML = `<div class="sr-msg" style="color:#ef4444;">Error: ${escHtml(d.error)}</div>`; return; }
248
+ if (!d.results || !d.results.length) { sr.innerHTML = '<div class="sr-msg">No results found.</div>'; return; }
249
+ sr.innerHTML = '';
250
+ d.results.forEach(res => {
251
+ const el = document.createElement('div');
252
+ el.className = 'sr-item';
253
+ el.innerHTML = `
254
+ <div class="sr-title">${escHtml(res.title)}</div>
255
+ <div class="sr-snip">${escHtml(res.snippet)}</div>
256
+ <div class="sr-url">${escHtml(res.url)}</div>
257
+ <div class="sr-actions">
258
+ <button class="sr-btn" onclick="addResultToCtx(${JSON.stringify(JSON.stringify(res))})">+ Context</button>
259
+ ${res.url ? `<button class="sr-btn" onclick="readLink(${JSON.stringify(res.url)})">Read Page</button>` : ''}
260
+ ${res.url ? `<button class="sr-btn" onclick="readMultiLinks(${JSON.stringify([res.url])})">Read + Chat</button>` : ''}
261
+ </div>`;
262
+ sr.appendChild(el);
263
+ });
264
+ } catch(e) { sr.innerHTML = `<div class="sr-msg" style="color:#ef4444;">Error: ${escHtml(e.message)}</div>`; }
265
+ }
266
+
267
+ function addResultToCtx(encoded) {
268
+ const res = JSON.parse(encoded);
269
+ setContext(`[${res.title}] ${res.snippet} β€” ${res.url}`);
270
+ }
271
+
272
+ async function readLink(url) {
273
+ const sr = document.getElementById('searchResults');
274
+ const msg = document.createElement('div');
275
+ msg.className = 'sr-msg';
276
+ msg.textContent = 'Reading ' + url + '…';
277
+ sr.insertBefore(msg, sr.firstChild);
278
+ try {
279
+ const r = await fetch('/api/read-link', {
280
+ method:'POST', headers:{'Content-Type':'application/json'},
281
+ body: JSON.stringify({ url })
282
+ });
283
+ const d = await r.json();
284
+ msg.remove();
285
+ if (d.pages && d.pages[0] && !d.pages[0].error) {
286
+ const pg = d.pages[0];
287
+ setContext(`[${pg.title || url}]\n${pg.text}`);
288
+ } else {
289
+ alert('Read error: ' + (d.pages?.[0]?.error || 'Unknown'));
290
+ }
291
+ } catch(e) { msg.remove(); alert('Error: ' + e.message); }
292
+ }
293
+
294
+ async function readMultiLinks(urls) {
295
+ const sr = document.getElementById('searchResults');
296
+ const msg = document.createElement('div');
297
+ msg.className = 'sr-msg';
298
+ msg.textContent = 'Reading ' + urls.length + ' page(s)…';
299
+ sr.insertBefore(msg, sr.firstChild);
300
+ try {
301
+ const r = await fetch('/api/read-link', {
302
+ method:'POST', headers:{'Content-Type':'application/json'},
303
+ body: JSON.stringify({ urls })
304
+ });
305
+ const d = await r.json();
306
+ msg.remove();
307
+ if (d.pages) {
308
+ d.pages.forEach(pg => {
309
+ if (!pg.error) setContext(`[${pg.title || pg.url}]\n${pg.text}`);
310
+ });
311
+ }
312
+ } catch(e) { msg.remove(); alert('Error: ' + e.message); }
313
+ }
314
+
315
+ async function loadModels() {
316
+ try {
317
+ const r = await fetch('/api/models');
318
+ const d = await r.json();
319
+ const list = document.getElementById('modelList');
320
+ list.innerHTML = '';
321
+ if (!d.models || !d.models.length) {
322
+ list.innerHTML = '<p style="color:#333;font-size:0.75rem;">No models found.</p>';
323
+ return;
324
+ }
325
+ d.models.forEach(m => {
326
+ const name = m.name || m;
327
+ const type = m.type || 'pt';
328
+ const isProt = name.toLowerCase() === 'exocore.pt';
329
+ const div = document.createElement('div');
330
+ div.className = 'model-card' + (selectedModel === name ? ' selected' : '');
331
+ div.onclick = () => selectModel(name, type);
332
+ div.innerHTML = `<div class="mn">${escHtml(name)}${isProt ? '<span class="pb">Protected</span>' : ''}</div>
333
+ <div class="mt">${type === 'exocore' ? 'ExocoreV1' : type}</div>`;
334
+ list.appendChild(div);
335
+ });
336
+ } catch(e) {}
337
+ }
338
+
339
+ function selectModel(name) {
340
+ selectedModel = name;
341
+ document.querySelectorAll('.model-card').forEach(el => {
342
+ const nm = el.querySelector('.mn').childNodes[0].nodeValue.trim();
343
+ el.classList.toggle('selected', nm === name);
344
+ });
345
+ document.getElementById('loadBtnWrap').style.display = '';
346
+ }
347
+
348
+ async function loadSelectedModel() {
349
+ if (!selectedModel) return;
350
+ const btn = document.getElementById('loadBtn');
351
+ btn.disabled = true; btn.textContent = 'Loading…';
352
+ try {
353
+ const r = await fetch('/api/load', {
354
+ method:'POST', headers:{'Content-Type':'application/json'},
355
+ body: JSON.stringify({ model: selectedModel, type: 'exocore' })
356
+ });
357
+ const d = await r.json();
358
+ if (d.error) alert(d.error);
359
+ else { loadedModel = selectedModel; updateStatusReady('ExocoreV1'); loadModelDetails(); }
360
+ } catch(e) { alert(e.message); }
361
+ btn.disabled = false; btn.textContent = 'Load Selected Model';
362
+ }
363
+
364
+ function updateStatusReady(name) {
365
+ document.getElementById('statusDot').className = 'dot ready';
366
+ document.getElementById('statusText').textContent = name || 'Ready';
367
+ const w = document.getElementById('welcome');
368
+ if (w) { w.innerHTML = '<h2 style="color:#a78bfa;">ExocoreV1</h2><p style="color:#555;">Model loaded Β· Start chatting</p>'; }
369
+ }
370
+
371
+ async function loadModelDetails() {
372
+ try {
373
+ const r = await fetch('/api/model-details');
374
+ const d = await r.json();
375
+ if (d.error || d.loading) return;
376
+ document.getElementById('detailsSection').style.display = '';
377
+ const cells = [
378
+ { v: (d.params_m||0)+'M', k:'Parameters' },
379
+ { v: d.num_layers||d.n_layer||'β€”', k:'Layers' },
380
+ { v: d.hidden_size||d.n_embd||'β€”', k:'Hidden' },
381
+ { v: d.num_heads||'β€”', k:'Attn Heads' },
382
+ { v: d.num_kv_heads||'β€”', k:'KV Heads' },
383
+ { v: d.vocab_size ? Math.round(d.vocab_size/1000)+'K' : 'β€”', k:'Vocab' },
384
+ ];
385
+ document.getElementById('detailsGrid').innerHTML = cells.map(c =>
386
+ `<div class="det-cell"><div class="dv">${c.v}</div><div class="dk">${c.k}</div></div>`).join('');
387
+ } catch(e) {}
388
+ }
389
+
390
+ async function checkAutoLoad() {
391
+ try {
392
+ const r = await fetch('/api/autoload-status');
393
+ const d = await r.json();
394
+ if (d.loading) {
395
+ document.getElementById('statusDot').className = 'dot loading';
396
+ document.getElementById('statusText').textContent = 'Loading ExocoreV1…';
397
+ document.getElementById('welcomeSub').textContent = 'ExocoreV1 is loading, please wait…';
398
+ setTimeout(checkAutoLoad, 2000);
399
+ } else if (d.done && d.model) {
400
+ loadedModel = d.model;
401
+ updateStatusReady('ExocoreV1');
402
+ loadModelDetails();
403
+ } else if (d.error) {
404
+ document.getElementById('statusDot').className = 'dot off';
405
+ document.getElementById('statusText').textContent = 'Load failed';
406
+ document.getElementById('welcomeSub').textContent = 'Select and load a model from the sidebar.';
407
+ } else {
408
+ document.getElementById('statusDot').className = 'dot off';
409
+ document.getElementById('statusText').textContent = 'No model loaded';
410
+ document.getElementById('welcomeSub').textContent = 'Select and load a model from the sidebar.';
411
+ }
412
+ } catch(e) { setTimeout(checkAutoLoad, 3000); }
413
+ }
414
+
415
+ function toggleThink(header) {
416
+ const content = header.nextElementSibling;
417
+ const collapsed = content.classList.toggle('collapsed');
418
+ header.querySelector('.think-toggle').textContent = collapsed ? 'β–Ά' : 'β–Ό';
419
+ }
420
+
421
+ function appendThinkBlock(parent, thinkText, mode) {
422
+ const label = mode === 'deep' ? 'Deep Think (Multi-Pass)' : 'Thinking';
423
+ const tb = document.createElement('div');
424
+ tb.className = 'think-block';
425
+ const maxH = Math.min(thinkText.length * 0.5, 300) + 'px';
426
+ tb.innerHTML = `<div class="think-header" onclick="toggleThink(this)">
427
+ <span class="th-label">${label}</span>
428
+ <span class="think-badge">⟳ Exocore reasoning</span>
429
+ <span class="think-toggle" style="margin-left:auto;">β–Ό</span>
430
+ </div>
431
+ <div class="think-content" style="max-height:${maxH};">
432
+ <div style="color:#4b5563;white-space:pre-wrap;">${escHtml(thinkText)}</div>
433
+ </div>`;
434
+ parent.appendChild(tb);
435
+ }
436
+
437
+ function addUserMessage(text) {
438
+ const w = document.getElementById('welcome');
439
+ if (w) w.remove();
440
+ const div = document.createElement('div');
441
+ div.className = 'msg user';
442
+ div.innerHTML = `<div class="msg-bubble">${escHtml(text)}</div>`;
443
+ document.getElementById('messages').appendChild(div);
444
+ div.scrollIntoView({ behavior:'smooth', block:'end' });
445
+ }
446
+
447
+ function addAssistantMessage() {
448
+ const div = document.createElement('div');
449
+ div.className = 'msg assistant';
450
+ const bubble = document.createElement('div');
451
+ bubble.className = 'msg-bubble';
452
+ div.appendChild(bubble);
453
+ document.getElementById('messages').appendChild(div);
454
+ div.scrollIntoView({ behavior:'smooth', block:'end' });
455
+ return { container: div, bubble };
456
+ }
457
+
458
+ async function sendMessage() {
459
+ if (isGenerating) return;
460
+ const input = document.getElementById('msgInput');
461
+ const text = input.value.trim();
462
+ if (!text) return;
463
+
464
+ isGenerating = true;
465
+ document.getElementById('sendBtn').disabled = true;
466
+ input.value = ''; input.style.height = 'auto';
467
+
468
+ addUserMessage(text);
469
+ const { container, bubble } = addAssistantMessage();
470
+ bubble.textContent = '…';
471
+
472
+ let fullText = '';
473
+ let thinkShown = false;
474
+
475
+ try {
476
+ const resp = await fetch('/api/chat', {
477
+ method:'POST', headers:{'Content-Type':'application/json'},
478
+ body: JSON.stringify({
479
+ message: text,
480
+ max_tokens: parseInt(document.getElementById('maxTokens').value),
481
+ temperature: parseFloat(document.getElementById('temperature').value),
482
+ mode: currentMode,
483
+ search_context: searchContext,
484
+ })
485
+ });
486
+
487
+ if (!resp.ok) {
488
+ const err = await resp.json().catch(()=>({}));
489
+ bubble.textContent = 'Error: ' + (err.error || resp.statusText);
490
+ return;
491
+ }
492
+
493
+ bubble.textContent = '';
494
+ const reader = resp.body.getReader();
495
+ const dec = new TextDecoder();
496
+ let buf = '';
497
+
498
+ while (true) {
499
+ const { done, value } = await reader.read();
500
+ if (done) break;
501
+ buf += dec.decode(value, { stream: true });
502
+ const lines = buf.split('\n\n');
503
+ buf = lines.pop();
504
+ for (const line of lines) {
505
+ if (!line.startsWith('data: ')) continue;
506
+ try {
507
+ const obj = JSON.parse(line.slice(6));
508
+ if (obj.error) { bubble.textContent = 'Error: ' + obj.error; break; }
509
+ if (obj.think_block && !thinkShown) {
510
+ thinkShown = true;
511
+ appendThinkBlock(container, obj.think_block, currentMode);
512
+ }
513
+ if (obj.token) {
514
+ fullText += obj.token;
515
+ bubble.textContent = fullText;
516
+ bubble.scrollIntoView({ behavior:'smooth', block:'end' });
517
+ }
518
+ } catch(e) {}
519
+ }
520
+ }
521
+ if (!fullText && !thinkShown) bubble.textContent = '(no response)';
522
+ if (searchContext) clearContext();
523
+ } catch(e) {
524
+ bubble.textContent = 'Error: ' + e.message;
525
+ } finally {
526
+ isGenerating = false;
527
+ document.getElementById('sendBtn').disabled = false;
528
+ }
529
+ }
530
+
531
+ async function clearChat() {
532
+ await fetch('/api/clear', { method:'POST' });
533
+ document.getElementById('messages').innerHTML =
534
+ `<div class="welcome" id="welcome"><h2 style="color:#a78bfa;">ExocoreV1</h2><p style="color:#555;">Chat cleared.</p></div>`;
535
+ clearContext();
536
+ }
537
+
538
+ async function initGemini() {
539
+ const r = await fetch('/api/gemini/init', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
540
+ const d = await r.json();
541
+ if (d.success) {
542
+ document.getElementById('geminiDot').className = 'gemini-dot on';
543
+ document.getElementById('geminiStatus').textContent = 'Ready';
544
+ } else {
545
+ document.getElementById('geminiStatus').textContent = d.error || 'Failed';
546
+ }
547
+ }
548
+
549
+ async function checkGemini() {
550
+ try {
551
+ const r = await fetch('/api/gemini/status');
552
+ const d = await r.json();
553
+ if (d.initialized) {
554
+ document.getElementById('geminiDot').className = 'gemini-dot on';
555
+ document.getElementById('geminiStatus').textContent = 'Ready';
556
+ }
557
+ } catch(e) {}
558
+ }
559
+
560
+ loadModels();
561
+ checkAutoLoad();
562
+ checkGemini();
563
+ </script>
564
+ </body>
565
+ </html>
templates/train.html ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ExocoreV1 β€” Train</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
8
+ <style>
9
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10
+ body { font-family: 'Segoe UI', sans-serif; background: #0d0d0f; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
11
+ header { background: #111116; border-bottom: 1px solid #222; padding: 10px 20px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
12
+ header h1 { font-size: 1.1rem; color: #a78bfa; font-weight: 700; letter-spacing: 1px; }
13
+ header nav a { color: #888; text-decoration: none; margin-left: 14px; font-size: 0.85rem; }
14
+ header nav a:hover { color: #a78bfa; }
15
+ .layout { display: flex; flex: 1; overflow: hidden; }
16
+ .left { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
17
+ .right { width: 360px; border-left: 1px solid #1e1e2a; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
18
+ .card { background: #111116; border: 1px solid #1e1e2a; border-radius: 10px; padding: 14px; }
19
+ .card h3 { font-size: 0.82rem; color: #c4b5fd; font-weight: 600; margin-bottom: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
20
+ .tabs { display: flex; gap: 4px; margin-bottom: 14px; }
21
+ .tab { background: #16161f; border: 1px solid #252535; color: #666; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; }
22
+ .tab.active { background: #1a1628; border-color: #a78bfa; color: #a78bfa; }
23
+ .fr { display: flex; flex-direction: column; gap: 3px; margin-bottom: 9px; }
24
+ .fr label { font-size: 0.7rem; color: #555; }
25
+ .fr input, .fr textarea, .fr select { background: #16161f; border: 1px solid #252535; color: #ddd; border-radius: 5px; padding: 7px 10px; font-size: 0.82rem; width: 100%; font-family: inherit; }
26
+ .fr input:focus, .fr textarea:focus { outline: none; border-color: #7c3aed; }
27
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
28
+ .btn { border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; font-size: 0.82rem; font-weight: 500; }
29
+ .btn-p { background: #7c3aed; color: white; }
30
+ .btn-p:hover { background: #6d28d9; }
31
+ .btn-p:disabled { background: #252535; color: #555; cursor: not-allowed; }
32
+ .btn-d { background: #4a1010; color: #fca5a5; }
33
+ .btn-d:hover { background: #5f1414; }
34
+ .btn-s { background: #16161f; border: 1px solid #252535; color: #888; }
35
+ .btn-s:hover { border-color: #a78bfa; color: #a78bfa; }
36
+ .btn-sm { padding: 4px 10px; font-size: 0.75rem; }
37
+ .progress-track { background: #16161f; border-radius: 20px; height: 6px; margin: 7px 0; overflow: hidden; }
38
+ .progress-fill { background: linear-gradient(90deg, #7c3aed, #a78bfa); height: 100%; border-radius: 20px; transition: width 0.3s; }
39
+ .log-box { background: #080810; border: 1px solid #1a1a24; border-radius: 5px; padding: 8px; font-family: monospace; font-size: 0.73rem; color: #666; max-height: 130px; overflow-y: auto; white-space: pre-wrap; }
40
+ .stat-row { display: flex; align-items: center; gap: 7px; font-size: 0.78rem; color: #666; margin-bottom: 7px; }
41
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: #333; flex-shrink: 0; }
42
+ .dot.running { background: #22c55e; animation: pulse 1s infinite; }
43
+ .dot.done { background: #22c55e; }
44
+ .dot.error { background: #ef4444; }
45
+ @keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} }
46
+ .mcard { background: #16161f; border: 1px solid #252535; border-radius: 7px; padding: 9px 12px; margin-bottom: 6px; display: flex; align-items: center; justify-content: space-between; }
47
+ .mcard .mn { font-size: 0.83rem; color: #ddd; font-weight: 500; }
48
+ .mcard .mt { font-size: 0.7rem; color: #555; margin-top: 2px; }
49
+ .pb { font-size: 0.68rem; padding: 2px 7px; border-radius: 20px; background: #1a1628; border: 1px solid #4c1d95; color: #a78bfa; margin-left: 5px; }
50
+ .chart-wrap { position: relative; height: 220px; margin-top: 8px; }
51
+ .arch-tree { font-size: 0.78rem; }
52
+ .arch-node { margin: 3px 0; display: flex; align-items: center; gap: 6px; }
53
+ .arch-node .label { background: #16161f; border: 1px solid #252535; border-radius: 5px; padding: 4px 10px; font-size: 0.75rem; color: #c4b5fd; flex: 1; }
54
+ .arch-node.block .label { border-color: #4c1d95; background: #1a1628; }
55
+ .arch-node.sub .label { border-color: #1e3a5f; background: #111820; color: #93c5fd; margin-left: 20px; font-size: 0.72rem; }
56
+ .arch-node.head-node .label { border-color: #134e2a; background: #0d1f16; color: #86efac; }
57
+ .arch-line { color: #333; font-size: 0.65rem; margin-left: 8px; }
58
+ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-thumb { background: #2a2a3a; border-radius: 3px; }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <header>
63
+ <h1>⬑ ExocoreV1</h1>
64
+ <nav><a href="/">Chat</a><a href="/train">Train</a></nav>
65
+ </header>
66
+ <div class="layout">
67
+ <!-- LEFT PANEL -->
68
+ <div class="left">
69
+ <div class="tabs">
70
+ <div class="tab active" onclick="showTab('train')">Train</div>
71
+ <div class="tab" onclick="showTab('auto')">Auto-Train (Gemini)</div>
72
+ </div>
73
+
74
+ <!-- TRAIN TAB -->
75
+ <div id="tab-train">
76
+ <div class="card">
77
+ <h3>Training Data</h3>
78
+ <div class="fr">
79
+ <label>Training Text</label>
80
+ <textarea id="trainText" rows="7" placeholder="Paste training text here (min 100 chars)…"></textarea>
81
+ </div>
82
+ <div style="display:flex;align-items:center;gap:8px;">
83
+ <button class="btn btn-s btn-sm" onclick="loadTrainTxt()">Load train.txt</button>
84
+ <span id="charCount" style="font-size:0.73rem;color:#555;"></span>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="card">
89
+ <h3>Model Configuration</h3>
90
+ <div class="fr"><label>Model Name</label><input type="text" id="modelName" value="Exocore"></div>
91
+ <div class="grid2">
92
+ <div class="fr"><label>Epochs</label><input type="number" id="epochs" value="60" min="1"></div>
93
+ <div class="fr"><label>Embedding Dim</label><input type="number" id="nEmbd" value="384" min="64"></div>
94
+ <div class="fr"><label>Layers</label><input type="number" id="nLayer" value="6" min="1"></div>
95
+ <div class="fr"><label>Block Size</label><input type="number" id="blockSize" value="256" min="32"></div>
96
+ <div class="fr"><label>Batch Size</label><input type="number" id="batchSize" value="16" min="1"></div>
97
+ <div class="fr"><label>Learning Rate</label><input type="number" id="lr" value="0.0002" step="0.00001"></div>
98
+ </div>
99
+ <div style="display:flex;gap:8px;">
100
+ <button class="btn btn-p" id="trainBtn" onclick="startTraining()">Start Training</button>
101
+ <button class="btn btn-d" id="stopBtn" onclick="stopTraining()" style="display:none;">Stop</button>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="card" id="trainStatusCard" style="display:none;">
106
+ <h3>Training Progress</h3>
107
+ <div class="stat-row">
108
+ <span class="dot" id="tDot"></span>
109
+ <span id="tStatus">Idle</span>
110
+ <span id="tEpoch" style="margin-left:auto;color:#555;font-size:0.73rem;"></span>
111
+ <span id="tLoss" style="color:#a78bfa;font-size:0.73rem;margin-left:8px;"></span>
112
+ </div>
113
+ <div class="progress-track"><div class="progress-fill" id="tProg" style="width:0%"></div></div>
114
+ <div class="chart-wrap"><canvas id="lossChart"></canvas></div>
115
+ <div class="log-box" id="tLog"></div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- AUTO TRAIN TAB -->
120
+ <div id="tab-auto" style="display:none;">
121
+ <div class="card">
122
+ <h3>Gemini Auto-Train</h3>
123
+ <p style="font-size:0.78rem;color:#555;margin-bottom:10px;">Gemini generates training data automatically, then ExocoreV1 trains on it.</p>
124
+ <div class="fr"><label>Initial Prompt</label><textarea id="autoPrompt" rows="3" placeholder="e.g. Explain quantum computing in detail."></textarea></div>
125
+ <div class="fr"><label>Continue Message</label><input type="text" id="autoContinue" value="ok next train"></div>
126
+ <div class="fr"><label>Model Name</label><input type="text" id="autoModel" value="Exocore"></div>
127
+ <div style="display:flex;gap:8px;">
128
+ <button class="btn btn-p" id="autoStartBtn" onclick="startAuto()">Start</button>
129
+ <button class="btn btn-d" id="autoStopBtn" onclick="stopAuto()" style="display:none;">Stop</button>
130
+ </div>
131
+ </div>
132
+ <div class="card" id="autoStatusCard" style="display:none;">
133
+ <h3>Auto-Train Status</h3>
134
+ <div class="stat-row"><span class="dot" id="aDot"></span><span id="aStatus">Idle</span></div>
135
+ <div class="log-box" id="aLog"></div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- RIGHT PANEL -->
141
+ <div class="right">
142
+ <!-- ARCHITECTURE TREE -->
143
+ <div class="card">
144
+ <h3>ExocoreV1 Architecture</h3>
145
+ <div class="arch-tree">
146
+ <div class="arch-node"><div class="label" style="text-align:center;color:#f59e0b;border-color:#78350f;background:#1c1208;">Input Tokens</div></div>
147
+ <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div>
148
+ <div class="arch-node"><div class="label">Token Embedding <span style="color:#555;">151,936 β†’ 1024</span></div></div>
149
+ <div style="text-align:center;color:#333;font-size:0.7rem;">↓ Γ—28 Transformer Blocks</div>
150
+
151
+ <div class="arch-node block">
152
+ <div class="label">Transformer Block (Γ—28)</div>
153
+ </div>
154
+ <div class="arch-node sub"><div class="label">Input LayerNorm <span style="color:#555;">[1024]</span></div></div>
155
+ <div class="arch-node sub"><div class="label">GQA Attention <span style="color:#555;">16Q / 8KV heads Β· dim 128</span></div></div>
156
+ <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">Q proj 1024β†’2048 Β· K,V 1024β†’1024 Β· O 2048β†’1024</div></div>
157
+ <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">Q-Norm & K-Norm (RMSNorm per head)</div></div>
158
+ <div class="arch-node sub" style="margin-left:38px;"><div class="label" style="font-size:0.68rem;color:#6b7280;">RoPE Β· ΞΈ=1,000,000 Β· max 40,960 tokens</div></div>
159
+ <div class="arch-node sub"><div class="label">Post-Attn LayerNorm <span style="color:#555;">[1024]</span></div></div>
160
+ <div class="arch-node sub"><div class="label">SwiGLU FFN <span style="color:#555;">1024β†’3072β†’1024</span></div></div>
161
+ <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div>
162
+
163
+ <div class="arch-node"><div class="label">Final RMSNorm <span style="color:#555;">[1024]</span></div></div>
164
+ <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div>
165
+ <div class="arch-node head-node"><div class="label">Language Model Head <span style="color:#555;">1024 β†’ 151,936</span></div></div>
166
+ <div style="text-align:center;color:#333;font-size:0.7rem;">↓</div>
167
+ <div class="arch-node"><div class="label" style="text-align:center;color:#f59e0b;border-color:#78350f;background:#1c1208;">Output Tokens</div></div>
168
+
169
+ <div style="margin-top:12px;padding-top:10px;border-top:1px solid #1e1e2a;">
170
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;">
171
+ <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">751.6M</div><div style="font-size:0.65rem;color:#555;">Parameters</div></div>
172
+ <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">28</div><div style="font-size:0.65rem;color:#555;">Layers</div></div>
173
+ <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">bfloat16</div><div style="font-size:0.65rem;color:#555;">Precision</div></div>
174
+ <div style="background:#16161f;border:1px solid #252535;border-radius:5px;padding:5px 8px;"><div style="font-size:0.82rem;color:#c4b5fd;font-weight:600;">40,960</div><div style="font-size:0.65rem;color:#555;">Max Context</div></div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <!-- TRAINED MODELS -->
181
+ <div class="card">
182
+ <h3>Trained Models</h3>
183
+ <div id="modelList"><p style="color:#444;font-size:0.78rem;">Loading…</p></div>
184
+ <button class="btn btn-s btn-sm" onclick="loadModels()" style="margin-top:8px;">Refresh</button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <script>
190
+ let trainInterval = null;
191
+ let autoInterval = null;
192
+ let lossChart = null;
193
+
194
+ function showTab(name) {
195
+ ['train','auto'].forEach(t => {
196
+ document.getElementById('tab-' + t).style.display = t === name ? '' : 'none';
197
+ });
198
+ document.querySelectorAll('.tab').forEach((el, i) => {
199
+ el.classList.toggle('active', ['train','auto'][i] === name);
200
+ });
201
+ }
202
+
203
+ async function loadTrainTxt() {
204
+ const r = await fetch('/api/train/text');
205
+ const d = await r.json();
206
+ if (d.found) {
207
+ document.getElementById('trainText').value = d.text;
208
+ document.getElementById('charCount').textContent = d.chars.toLocaleString() + ' chars';
209
+ } else { alert('train.txt not found.'); }
210
+ }
211
+
212
+ document.getElementById('trainText').addEventListener('input', function() {
213
+ document.getElementById('charCount').textContent = this.value.length.toLocaleString() + ' chars';
214
+ });
215
+
216
+ function initChart() {
217
+ const ctx = document.getElementById('lossChart').getContext('2d');
218
+ lossChart = new Chart(ctx, {
219
+ type: 'line',
220
+ data: {
221
+ labels: [],
222
+ datasets: [{
223
+ label: 'Training Loss',
224
+ data: [],
225
+ borderColor: '#a78bfa',
226
+ backgroundColor: 'rgba(167,139,250,0.08)',
227
+ borderWidth: 2,
228
+ pointRadius: 3,
229
+ pointBackgroundColor: '#a78bfa',
230
+ tension: 0.3,
231
+ fill: true,
232
+ }]
233
+ },
234
+ options: {
235
+ responsive: true, maintainAspectRatio: false,
236
+ plugins: { legend: { labels: { color: '#888', font: { size: 11 } } } },
237
+ scales: {
238
+ x: { grid: { color: '#1a1a24' }, ticks: { color: '#555', font: { size: 10 } }, title: { display: true, text: 'Epoch', color: '#555', font: { size: 10 } } },
239
+ y: { grid: { color: '#1a1a24' }, ticks: { color: '#555', font: { size: 10 } }, title: { display: true, text: 'Loss', color: '#555', font: { size: 10 } } },
240
+ },
241
+ animation: { duration: 200 }
242
+ }
243
+ });
244
+ }
245
+
246
+ function updateChart(history) {
247
+ if (!lossChart) initChart();
248
+ lossChart.data.labels = history.map(h => 'E' + h.epoch);
249
+ lossChart.data.datasets[0].data = history.map(h => h.loss);
250
+ lossChart.update('none');
251
+ }
252
+
253
+ async function startTraining() {
254
+ const text = document.getElementById('trainText').value;
255
+ if (text.length < 100) { alert('Need at least 100 characters of training text.'); return; }
256
+ const config = {
257
+ epochs: parseInt(document.getElementById('epochs').value),
258
+ n_embd: parseInt(document.getElementById('nEmbd').value),
259
+ n_layer: parseInt(document.getElementById('nLayer').value),
260
+ block_size: parseInt(document.getElementById('blockSize').value),
261
+ batch_size: parseInt(document.getElementById('batchSize').value),
262
+ lr: parseFloat(document.getElementById('lr').value),
263
+ };
264
+ const model_name = document.getElementById('modelName').value || 'Exocore';
265
+ const r = await fetch('/api/train/start', {
266
+ method: 'POST', headers: {'Content-Type':'application/json'},
267
+ body: JSON.stringify({ text, config, model_name })
268
+ });
269
+ const d = await r.json();
270
+ if (d.error) { alert(d.error); return; }
271
+ document.getElementById('trainStatusCard').style.display = '';
272
+ document.getElementById('trainBtn').disabled = true;
273
+ document.getElementById('stopBtn').style.display = '';
274
+ if (!lossChart) initChart();
275
+ lossChart.data.labels = [];
276
+ lossChart.data.datasets[0].data = [];
277
+ lossChart.update('none');
278
+ trainInterval = setInterval(pollTrain, 1500);
279
+ }
280
+
281
+ async function stopTraining() {
282
+ await fetch('/api/train/stop', { method: 'POST' });
283
+ }
284
+
285
+ async function pollTrain() {
286
+ const r = await fetch('/api/train/status');
287
+ const d = await r.json();
288
+ const dot = document.getElementById('tDot');
289
+ document.getElementById('tStatus').textContent = d.status || (d.running ? 'Training…' : (d.error ? 'Error' : 'Done'));
290
+ dot.className = 'dot ' + (d.running ? 'running' : (d.error ? 'error' : 'done'));
291
+ const pct = d.total_epochs > 0 ? (d.epoch / d.total_epochs * 100) : 0;
292
+ document.getElementById('tProg').style.width = pct + '%';
293
+ document.getElementById('tEpoch').textContent = d.total_epochs ? `Epoch ${d.epoch}/${d.total_epochs}` : '';
294
+ document.getElementById('tLoss').textContent = d.loss ? `Loss: ${d.loss}` : '';
295
+ if (d.loss_history && d.loss_history.length) updateChart(d.loss_history);
296
+ if (d.log && d.log.length) {
297
+ const lb = document.getElementById('tLog');
298
+ lb.textContent = d.log.slice(-30).join('\n');
299
+ lb.scrollTop = lb.scrollHeight;
300
+ }
301
+ if (!d.running) {
302
+ clearInterval(trainInterval);
303
+ document.getElementById('trainBtn').disabled = false;
304
+ document.getElementById('stopBtn').style.display = 'none';
305
+ if (d.loss_history) updateChart(d.loss_history);
306
+ loadModels();
307
+ }
308
+ }
309
+
310
+ async function startAuto() {
311
+ const prompt = document.getElementById('autoPrompt').value.trim();
312
+ if (!prompt) { alert('Enter an initial prompt.'); return; }
313
+ const r = await fetch('/api/gemini/autotrain/start', {
314
+ method: 'POST', headers: {'Content-Type':'application/json'},
315
+ body: JSON.stringify({
316
+ initial_prompt: prompt,
317
+ continue_msg: document.getElementById('autoContinue').value,
318
+ model_name: document.getElementById('autoModel').value,
319
+ config: {}
320
+ })
321
+ });
322
+ const d = await r.json();
323
+ if (d.error) { alert(d.error); return; }
324
+ document.getElementById('autoStatusCard').style.display = '';
325
+ document.getElementById('autoStartBtn').disabled = true;
326
+ document.getElementById('autoStopBtn').style.display = '';
327
+ autoInterval = setInterval(pollAuto, 2000);
328
+ }
329
+ async function stopAuto() {
330
+ await fetch('/api/gemini/autotrain/stop', { method: 'POST' });
331
+ }
332
+ async function pollAuto() {
333
+ const r = await fetch('/api/gemini/autotrain/status');
334
+ const d = await r.json();
335
+ document.getElementById('aDot').className = 'dot ' + (d.running ? 'running' : (d.error ? 'error' : 'done'));
336
+ document.getElementById('aStatus').textContent = d.status || (d.running ? 'Running…' : 'Done');
337
+ if (d.log && d.log.length) {
338
+ const lb = document.getElementById('aLog');
339
+ lb.textContent = d.log.slice(-20).join('\n');
340
+ lb.scrollTop = lb.scrollHeight;
341
+ }
342
+ if (!d.running) {
343
+ clearInterval(autoInterval);
344
+ document.getElementById('autoStartBtn').disabled = false;
345
+ document.getElementById('autoStopBtn').style.display = 'none';
346
+ loadModels();
347
+ }
348
+ }
349
+
350
+ const PROTECTED = ['exocore.pt'];
351
+
352
+ async function loadModels() {
353
+ const r = await fetch('/api/train/models');
354
+ const d = await r.json();
355
+ const list = document.getElementById('modelList');
356
+ if (!d.models || !d.models.length) {
357
+ list.innerHTML = '<p style="color:#444;font-size:0.78rem;">No trained models yet.</p>';
358
+ return;
359
+ }
360
+ list.innerHTML = '';
361
+ d.models.forEach(m => {
362
+ const name = typeof m === 'string' ? m : (m.name || m);
363
+ const isProt = PROTECTED.includes(name.toLowerCase());
364
+ const div = document.createElement('div');
365
+ div.className = 'mcard';
366
+ div.innerHTML = `
367
+ <div>
368
+ <div class="mn">${name}${isProt ? '<span class="pb">Protected</span>' : ''}</div>
369
+ <div class="mt">ExocoreV1 model</div>
370
+ </div>
371
+ <div style="display:flex;gap:5px;">
372
+ <button class="btn btn-s btn-sm" onclick="viewInfo('${name}')">Info</button>
373
+ ${isProt ? '' : `<button class="btn btn-d btn-sm" onclick="deleteModel('${name}')">Delete</button>`}
374
+ </div>`;
375
+ list.appendChild(div);
376
+ });
377
+ }
378
+
379
+ async function viewInfo(name) {
380
+ const r = await fetch('/api/train/model-info/' + encodeURIComponent(name));
381
+ const d = await r.json();
382
+ if (d.error) { alert('Error: ' + d.error); return; }
383
+ const lines = Object.entries(d).map(([k,v]) => `${k}: ${JSON.stringify(v)}`).join('\n');
384
+ alert(lines);
385
+ }
386
+
387
+ async function deleteModel(name) {
388
+ if (!confirm(`Delete "${name}"?`)) return;
389
+ const r = await fetch('/api/model/delete', {
390
+ method: 'POST', headers: {'Content-Type':'application/json'},
391
+ body: JSON.stringify({ local_name: name })
392
+ });
393
+ const d = await r.json();
394
+ if (d.error) { alert('Error: ' + d.error); }
395
+ else { loadModels(); }
396
+ }
397
+
398
+ loadModels();
399
+ </script>
400
+ </body>
401
+ </html>
trainer.py CHANGED
@@ -171,7 +171,6 @@ def start_training(text, config, model_name):
171
  itos = {i: c for i, c in enumerate(chars)}
172
 
173
  data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
174
-
175
  steps_per_epoch = max(1, len(data) // (block_size * batch_size))
176
  total_steps = steps_per_epoch * epochs
177
  training_state["total_steps"] = total_steps
@@ -197,7 +196,6 @@ def start_training(text, config, model_name):
197
  for step in range(steps_per_epoch):
198
  if _stop_flag.is_set():
199
  break
200
-
201
  xb, yb = get_batch(data, block_size, batch_size, device)
202
  _, loss = model(xb, yb)
203
  optimizer.zero_grad()
@@ -205,7 +203,6 @@ def start_training(text, config, model_name):
205
  nn.utils.clip_grad_norm_(model.parameters(), 1.0)
206
  optimizer.step()
207
  scheduler.step()
208
-
209
  global_step += 1
210
  epoch_loss += loss.item()
211
  training_state["step"] = global_step
@@ -269,459 +266,9 @@ def get_trained_models():
269
  return results
270
 
271
 
272
- _inference_cache = {}
273
-
274
- # ── Qwen3 β†’ Exocore.pt conversion ────────────────────────────────────────────
275
-
276
- _convert_state = {
277
- "running": False, "step": "", "progress": 0,
278
- "done": False, "error": None, "result": None,
279
- }
280
-
281
-
282
- def get_convert_state():
283
- return _convert_state
284
-
285
-
286
- def start_qwen3_conversion(repo_id="Qwen/Qwen3-0.6B", out_name="Exocore"):
287
- global _convert_state
288
- _convert_state = {
289
- "running": True, "step": "Starting...", "progress": 0,
290
- "done": False, "error": None, "result": None,
291
- }
292
-
293
- def run():
294
- import shutil
295
- import json as _json
296
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
297
- hf_dir = os.path.join(save_dir, "_qwen3_tmp")
298
- out_path = os.path.join(save_dir, f"{out_name}.pt")
299
-
300
- try:
301
- # ── Step 1: Download ──────────────────────────────────────────────
302
- _convert_state["step"] = "Downloading Qwen3-0.6B (model weights ~1.1 GB)..."
303
- _convert_state["progress"] = 5
304
- from huggingface_hub import snapshot_download
305
- os.makedirs(hf_dir, exist_ok=True)
306
- snapshot_download(
307
- repo_id=repo_id,
308
- local_dir=hf_dir,
309
- ignore_patterns=["*.bin", "flax_model*", "tf_model*", "rust_model*", "*.gguf"],
310
- )
311
-
312
- # ── Step 2: Load tokenizer JSON ───────────────────────────────────
313
- _convert_state["step"] = "Loading tokenizer..."
314
- _convert_state["progress"] = 40
315
- tok_path = os.path.join(hf_dir, "tokenizer.json")
316
- tokcfg_path = os.path.join(hf_dir, "tokenizer_config.json")
317
- with open(tok_path, "r", encoding="utf-8") as f:
318
- tokenizer_json = f.read()
319
- with open(tokcfg_path, "r", encoding="utf-8") as f:
320
- tokenizer_config_json = f.read()
321
-
322
- # ── Step 3: Load HF config ────────────────────────────────────────
323
- _convert_state["step"] = "Reading model config..."
324
- _convert_state["progress"] = 45
325
- cfg_path = os.path.join(hf_dir, "config.json")
326
- with open(cfg_path, "r") as f:
327
- hf_cfg = _json.load(f)
328
- from qwen3_model import QWEN3_06B_CONFIG
329
- model_cfg = {
330
- "hidden_size": hf_cfg.get("hidden_size", QWEN3_06B_CONFIG["hidden_size"]),
331
- "intermediate_size": hf_cfg.get("intermediate_size", QWEN3_06B_CONFIG["intermediate_size"]),
332
- "num_hidden_layers": hf_cfg.get("num_hidden_layers", QWEN3_06B_CONFIG["num_hidden_layers"]),
333
- "num_attention_heads": hf_cfg.get("num_attention_heads", QWEN3_06B_CONFIG["num_attention_heads"]),
334
- "num_key_value_heads": hf_cfg.get("num_key_value_heads", QWEN3_06B_CONFIG["num_key_value_heads"]),
335
- "head_dim": hf_cfg.get("head_dim", QWEN3_06B_CONFIG["head_dim"]),
336
- "vocab_size": hf_cfg.get("vocab_size", QWEN3_06B_CONFIG["vocab_size"]),
337
- "rms_norm_eps": hf_cfg.get("rms_norm_eps", QWEN3_06B_CONFIG["rms_norm_eps"]),
338
- "rope_theta": hf_cfg.get("rope_theta", QWEN3_06B_CONFIG["rope_theta"]),
339
- "max_position_embeddings": hf_cfg.get("max_position_embeddings", QWEN3_06B_CONFIG["max_position_embeddings"]),
340
- }
341
-
342
- # ── Step 4: Load safetensors weights ─��────────────────────────────
343
- _convert_state["step"] = "Loading model weights from safetensors..."
344
- _convert_state["progress"] = 50
345
- import glob as _glob
346
- sf_files = sorted(_glob.glob(os.path.join(hf_dir, "*.safetensors")))
347
- if not sf_files:
348
- raise FileNotFoundError("No .safetensors file found after download")
349
- from safetensors.torch import load_file as sf_load
350
- hf_state = {}
351
- for sf in sf_files:
352
- hf_state.update(sf_load(sf, device="cpu"))
353
-
354
- # ── Step 5: Remap keys ────────────────────────────────────────────
355
- _convert_state["step"] = "Remapping weights to Exocore format..."
356
- _convert_state["progress"] = 70
357
- from qwen3_model import remap_hf_keys
358
- exocore_state = remap_hf_keys(hf_state)
359
-
360
- # ── Step 6: Verify weights load into our model ────────────────────
361
- _convert_state["step"] = "Verifying model integrity..."
362
- _convert_state["progress"] = 80
363
- from qwen3_model import Qwen3ForCausalLM
364
- model = Qwen3ForCausalLM(model_cfg)
365
- missing, unexpected = model.load_state_dict(exocore_state, strict=False)
366
- computed_buffers = {"rope_cos", "rope_sin"}
367
- real_missing = [k for k in missing if k not in computed_buffers]
368
- if real_missing:
369
- raise RuntimeError(f"Missing parameter weights: {real_missing[:5]}")
370
-
371
- total_params = sum(p.numel() for p in model.parameters())
372
-
373
- # ── Step 7: Save as Exocore.pt ────────────────────────────────────
374
- _convert_state["step"] = f"Saving Exocore.pt ({total_params/1e6:.0f}M params)..."
375
- _convert_state["progress"] = 88
376
- torch.save({
377
- "model_state": exocore_state,
378
- "config": model_cfg,
379
- "tokenizer_json": tokenizer_json,
380
- "tokenizer_config": tokenizer_config_json,
381
- "model_name": out_name,
382
- "exocore_type": "qwen3",
383
- "total_params": total_params,
384
- }, out_path)
385
-
386
- # ── Step 8: Delete HF temp dir ────────────────────────────────────
387
- _convert_state["step"] = "Cleaning up HF files..."
388
- _convert_state["progress"] = 96
389
- shutil.rmtree(hf_dir, ignore_errors=True)
390
-
391
- # Write tiny meta file so is_qwen3_pt() is instant
392
- import json as _json2
393
- with open(out_path + ".meta", "w") as mf:
394
- _json2.dump({"exocore_type": "qwen3", "params": total_params}, mf)
395
-
396
- size_mb = os.path.getsize(out_path) / 1024 / 1024
397
- _convert_state["step"] = f"Done! Saved {out_name}.pt ({size_mb:.0f} MB, {total_params/1e6:.0f}M params)"
398
- _convert_state["progress"] = 100
399
- _convert_state["done"] = True
400
- _convert_state["running"] = False
401
- _convert_state["result"] = {"name": f"{out_name}.pt", "size_mb": round(size_mb, 1), "params": total_params}
402
-
403
- except Exception as e:
404
- shutil.rmtree(hf_dir, ignore_errors=True)
405
- _convert_state["error"] = str(e)
406
- _convert_state["running"] = False
407
- _convert_state["done"] = True
408
- _convert_state["step"] = f"Error: {e}"
409
-
410
- t = threading.Thread(target=run, daemon=True)
411
- t.start()
412
-
413
-
414
- # ── Qwen3 inference ───────────────────────────────────────────────────────────
415
-
416
- _qwen3_cache = {}
417
-
418
-
419
- def load_qwen3_model(model_name):
420
- import gc
421
- if model_name in _qwen3_cache:
422
- return _qwen3_cache[model_name]
423
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
424
- path = os.path.join(save_dir, model_name)
425
-
426
- ckpt = torch.load(path, map_location="cpu", weights_only=False)
427
- cfg = ckpt["config"]
428
- tok_json = ckpt["tokenizer_json"]
429
-
430
- # Create model in bfloat16 from the start to avoid float32 β†’ bfloat16 doubling
431
- from qwen3_model import Qwen3ForCausalLM
432
- _prev_dtype = torch.get_default_dtype()
433
- torch.set_default_dtype(torch.bfloat16)
434
- try:
435
- model = Qwen3ForCausalLM(cfg)
436
- finally:
437
- torch.set_default_dtype(_prev_dtype)
438
-
439
- model.load_state_dict(ckpt["model_state"], strict=False)
440
- del ckpt # free ~1.5 GB checkpoint from RAM immediately
441
- gc.collect()
442
-
443
- model.eval()
444
- from tokenizers import Tokenizer
445
- tok = Tokenizer.from_str(tok_json)
446
- _qwen3_cache[model_name] = {"model": model, "tokenizer": tok, "config": cfg}
447
- return _qwen3_cache[model_name]
448
-
449
-
450
- def is_qwen3_pt(model_name):
451
- """Return True if this .pt file is a converted Qwen3 model (uses tiny .meta file for speed)."""
452
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
453
- meta_path = os.path.join(save_dir, model_name + ".meta")
454
- if os.path.exists(meta_path):
455
- try:
456
- with open(meta_path, "r") as f:
457
- meta = json.load(f)
458
- return meta.get("exocore_type") == "qwen3"
459
- except Exception:
460
- pass
461
- return False
462
-
463
-
464
- def generate_qwen3_stream(model_name, messages, max_tokens=512, temperature=0.7, think_mode=False):
465
- from qwen3_model import build_chat_prompt, IM_END, EOT
466
- mc = load_qwen3_model(model_name)
467
- tok = mc["tokenizer"]
468
- model = mc["model"]
469
-
470
- prompt = build_chat_prompt(messages, think=think_mode)
471
- enc = tok.encode(prompt)
472
- ids = torch.tensor([enc.ids], dtype=torch.long)
473
-
474
- buf = "" # rolling buffer
475
- in_think = False # currently inside <think>...</think>
476
- think_done = False # think block already processed
477
- think_yielded = False
478
-
479
- with torch.no_grad():
480
- for _ in range(max_tokens):
481
- ctx = ids[:, -min(ids.shape[1], mc["config"]["max_position_embeddings"]):]
482
- logits, _ = model(ctx)
483
- logits = logits[:, -1, :] / max(temperature, 1e-5)
484
- v, _ = torch.topk(logits, min(50, logits.size(-1)))
485
- logits[logits < v[:, [-1]]] = float("-inf")
486
- probs = torch.softmax(logits, dim=-1)
487
- nxt = torch.multinomial(probs, 1)
488
- tok_id = nxt.item()
489
- ids = torch.cat([ids, nxt], dim=1)
490
-
491
- if tok_id in (IM_END, EOT):
492
- break
493
-
494
- buf += tok.decode([tok_id])
495
-
496
- # Detect opening <think>
497
- if not think_done and not in_think and "<think>" in buf:
498
- pre, buf = buf.split("<think>", 1)
499
- if pre.strip():
500
- yield ("token", pre)
501
- in_think = True
502
-
503
- # Inside think block β€” wait for </think>
504
- if in_think:
505
- if "</think>" in buf:
506
- think_text, buf = buf.split("</think>", 1)
507
- think_done = True
508
- in_think = False
509
- # Only yield think_block in thinking mode
510
- if think_mode and not think_yielded:
511
- yield ("think_block", think_text.strip())
512
- think_yielded = True
513
- # fall through to emit buf below
514
- else:
515
- continue # still collecting think content
516
-
517
- # Emit content outside think blocks
518
- if not in_think:
519
- if not think_done:
520
- # Guard last 7 chars to catch a partial "<think>" opening
521
- if len(buf) > 7:
522
- safe, buf = buf[:-7], buf[-7:]
523
- yield ("token", safe)
524
- else:
525
- yield ("token", buf)
526
- buf = ""
527
-
528
- # Flush any remaining buffer
529
- if buf and not in_think:
530
- yield ("token", buf)
531
-
532
-
533
- # ── HuggingFace model support ─────────────────────────────────────────────────
534
-
535
- _hf_cache = {}
536
- _hf_download_state = {"running": False, "progress": "", "done": False, "error": None}
537
-
538
-
539
- def get_hf_download_state():
540
- return _hf_download_state
541
-
542
-
543
- def download_hf_model(repo_id, local_name):
544
- global _hf_download_state
545
- _hf_download_state = {"running": True, "progress": f"Downloading {repo_id}...", "done": False, "error": None}
546
-
547
- def run():
548
- try:
549
- from huggingface_hub import snapshot_download
550
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained", local_name)
551
- os.makedirs(save_dir, exist_ok=True)
552
- _hf_download_state["progress"] = f"Fetching {repo_id} β†’ models/trained/{local_name}/"
553
- snapshot_download(
554
- repo_id=repo_id,
555
- local_dir=save_dir,
556
- ignore_patterns=["*.bin", "*.msgpack", "flax_model*", "tf_model*", "rust_model*"],
557
- )
558
- _hf_download_state["progress"] = "Done"
559
- _hf_download_state["done"] = True
560
- _hf_download_state["running"] = False
561
- except Exception as e:
562
- _hf_download_state["error"] = str(e)
563
- _hf_download_state["running"] = False
564
- _hf_download_state["done"] = True
565
-
566
- t = threading.Thread(target=run, daemon=True)
567
- t.start()
568
-
569
-
570
- def is_hf_model(model_name):
571
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
572
- path = os.path.join(save_dir, model_name)
573
- return os.path.isdir(path) and os.path.exists(os.path.join(path, "config.json"))
574
-
575
-
576
- def load_hf_model(model_name):
577
- if model_name in _hf_cache:
578
- return _hf_cache[model_name]
579
- import torch
580
- from transformers import AutoModelForCausalLM, AutoTokenizer
581
- save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
582
- path = os.path.join(save_dir, model_name)
583
- tokenizer = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
584
- model = AutoModelForCausalLM.from_pretrained(
585
- path,
586
- torch_dtype=torch.bfloat16,
587
- device_map="cpu",
588
- trust_remote_code=True,
589
- )
590
- model.eval()
591
- _hf_cache[model_name] = {"model": model, "tokenizer": tokenizer}
592
- return _hf_cache[model_name]
593
 
594
-
595
- def generate_hf_text(model_name, messages, max_tokens=512, temperature=0.7, think_mode=False):
596
- import torch
597
- from transformers import TextIteratorStreamer
598
- import queue as _queue
599
-
600
- mc = load_hf_model(model_name)
601
- model = mc["model"]
602
- tokenizer = mc["tokenizer"]
603
-
604
- try:
605
- text = tokenizer.apply_chat_template(
606
- messages,
607
- tokenize=False,
608
- add_generation_prompt=True,
609
- enable_thinking=think_mode,
610
- )
611
- except Exception:
612
- text = tokenizer.apply_chat_template(
613
- messages,
614
- tokenize=False,
615
- add_generation_prompt=True,
616
- )
617
-
618
- inputs = tokenizer([text], return_tensors="pt")
619
-
620
- streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=False)
621
-
622
- gen_kwargs = dict(
623
- **inputs,
624
- max_new_tokens=max_tokens,
625
- temperature=temperature if temperature > 0 else 1.0,
626
- do_sample=temperature > 0.05,
627
- streamer=streamer,
628
- )
629
-
630
- gen_thread = threading.Thread(target=model.generate, kwargs=gen_kwargs, daemon=True)
631
- gen_thread.start()
632
-
633
- full = ""
634
- for chunk in streamer:
635
- full += chunk
636
-
637
- gen_thread.join(timeout=5)
638
-
639
- full = full.strip()
640
-
641
- if think_mode and "<think>" in full:
642
- if "</think>" in full:
643
- think_part = full.split("<think>", 1)[1].split("</think>", 1)[0].strip()
644
- answer_part = full.split("</think>", 1)[1].strip()
645
- else:
646
- think_part = full.replace("<think>", "").strip()
647
- answer_part = full.replace("<think>", "").strip()
648
- return {"think_block": think_part, "response": answer_part, "thinking": True}
649
- else:
650
- clean = full.replace("<think>", "").replace("</think>", "").strip()
651
- return {"response": clean, "thinking": False}
652
-
653
-
654
- def generate_hf_stream(model_name, messages, max_tokens=512, temperature=0.7, think_mode=False):
655
- import torch
656
- from transformers import TextIteratorStreamer
657
-
658
- mc = load_hf_model(model_name)
659
- model = mc["model"]
660
- tokenizer = mc["tokenizer"]
661
-
662
- try:
663
- text = tokenizer.apply_chat_template(
664
- messages,
665
- tokenize=False,
666
- add_generation_prompt=True,
667
- enable_thinking=think_mode,
668
- )
669
- except Exception:
670
- text = tokenizer.apply_chat_template(
671
- messages,
672
- tokenize=False,
673
- add_generation_prompt=True,
674
- )
675
-
676
- inputs = tokenizer([text], return_tensors="pt")
677
-
678
- streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=False)
679
- gen_kwargs = dict(
680
- **inputs,
681
- max_new_tokens=max_tokens,
682
- temperature=temperature if temperature > 0 else 1.0,
683
- do_sample=temperature > 0.05,
684
- streamer=streamer,
685
- )
686
- gen_thread = threading.Thread(target=model.generate, kwargs=gen_kwargs, daemon=True)
687
- gen_thread.start()
688
-
689
- buffer = ""
690
- in_think = False
691
- think_done = False
692
- think_buf = ""
693
- resp_buf = ""
694
-
695
- for chunk in streamer:
696
- buffer += chunk
697
-
698
- if think_mode and not think_done:
699
- if not in_think and "<think>" in buffer:
700
- in_think = True
701
- buffer = buffer.split("<think>", 1)[1]
702
- if in_think:
703
- if "</think>" in buffer:
704
- parts = buffer.split("</think>", 1)
705
- think_buf += parts[0]
706
- buffer = parts[1]
707
- in_think = False
708
- think_done = True
709
- yield ("think_block", think_buf.strip())
710
- for ch in buffer:
711
- yield ("token", ch)
712
- buffer = ""
713
- else:
714
- think_buf += buffer
715
- buffer = ""
716
- continue
717
-
718
- for ch in buffer:
719
- yield ("token", ch)
720
- buffer = ""
721
-
722
- if buffer:
723
- for ch in buffer:
724
- yield ("token", ch)
725
 
726
 
727
  def load_trained_model(model_name):
@@ -757,11 +304,7 @@ def generate_text(model_name, prompt, max_tokens=200, temperature=0.8, top_k=40,
757
  itos = mc["itos"]
758
  block_size = mc["block_size"]
759
 
760
- if think_mode:
761
- formatted = f"User: {prompt}\n<think>"
762
- else:
763
- formatted = prompt
764
-
765
  encoded = [stoi.get(c, 0) for c in formatted]
766
  idx = torch.tensor([encoded], dtype=torch.long)
767
  out = model.generate(idx, max_new_tokens=max_tokens, temperature=temperature, top_k=top_k)
@@ -772,11 +315,153 @@ def generate_text(model_name, prompt, max_tokens=200, temperature=0.8, top_k=40,
772
  if think_mode:
773
  if "</think>" in generated:
774
  parts = generated.split("</think>", 1)
775
- think_block = parts[0].strip()
776
- response = parts[1].strip()
777
- else:
778
- think_block = generated.strip()
779
- response = generated.strip()
780
- return {"think_block": think_block, "response": response, "thinking": True}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  else:
782
- return {"response": generated, "thinking": False}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  itos = {i: c for i, c in enumerate(chars)}
172
 
173
  data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
 
174
  steps_per_epoch = max(1, len(data) // (block_size * batch_size))
175
  total_steps = steps_per_epoch * epochs
176
  training_state["total_steps"] = total_steps
 
196
  for step in range(steps_per_epoch):
197
  if _stop_flag.is_set():
198
  break
 
199
  xb, yb = get_batch(data, block_size, batch_size, device)
200
  _, loss = model(xb, yb)
201
  optimizer.zero_grad()
 
203
  nn.utils.clip_grad_norm_(model.parameters(), 1.0)
204
  optimizer.step()
205
  scheduler.step()
 
206
  global_step += 1
207
  epoch_loss += loss.item()
208
  training_state["step"] = global_step
 
266
  return results
267
 
268
 
269
+ # ── MiniGPT inference ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ _inference_cache = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
 
274
  def load_trained_model(model_name):
 
304
  itos = mc["itos"]
305
  block_size = mc["block_size"]
306
 
307
+ formatted = f"User: {prompt}\n<think>" if think_mode else prompt
 
 
 
 
308
  encoded = [stoi.get(c, 0) for c in formatted]
309
  idx = torch.tensor([encoded], dtype=torch.long)
310
  out = model.generate(idx, max_new_tokens=max_tokens, temperature=temperature, top_k=top_k)
 
315
  if think_mode:
316
  if "</think>" in generated:
317
  parts = generated.split("</think>", 1)
318
+ return {"think_block": parts[0].strip(), "response": parts[1].strip(), "thinking": True}
319
+ return {"think_block": generated.strip(), "response": generated.strip(), "thinking": True}
320
+ return {"response": generated, "thinking": False}
321
+
322
+
323
+ # ── ExocoreV1 inference ───────────────────────────────────────────────────────
324
+
325
+ _exocore_cache = {}
326
+
327
+
328
+ def is_exocore_pt(model_name):
329
+ save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
330
+ meta_path = os.path.join(save_dir, model_name + ".meta")
331
+ if os.path.exists(meta_path):
332
+ try:
333
+ with open(meta_path, "r") as f:
334
+ meta = json.load(f)
335
+ return meta.get("exocore_type") in ("qwen3", "exocoreV1")
336
+ except Exception:
337
+ pass
338
+ path = os.path.join(save_dir, model_name)
339
+ if os.path.exists(path) and path.endswith(".pt"):
340
+ try:
341
+ ckpt = torch.load(path, map_location="cpu", weights_only=False)
342
+ return ckpt.get("exocore_type") in ("qwen3", "exocoreV1")
343
+ except Exception:
344
+ pass
345
+ return False
346
+
347
+
348
+ def load_exocore_model(model_name):
349
+ import gc
350
+ if model_name in _exocore_cache:
351
+ return _exocore_cache[model_name]
352
+ from exocore_model import ExocoreLM
353
+ save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
354
+ path = os.path.join(save_dir, model_name)
355
+ ckpt = torch.load(path, map_location="cpu", weights_only=False)
356
+ cfg = ckpt["config"]
357
+ tok_json = ckpt["tokenizer_json"]
358
+ model = ExocoreLM(cfg)
359
+ model.load_state_dict(ckpt["model_state"], strict=True)
360
+ del ckpt
361
+ gc.collect()
362
+ model.eval()
363
+ from tokenizers import Tokenizer
364
+ tok = Tokenizer.from_str(tok_json)
365
+ _exocore_cache[model_name] = {"model": model, "tokenizer": tok, "config": cfg, "name": "ExocoreV1"}
366
+ return _exocore_cache[model_name]
367
+
368
+
369
+ def generate_exocore_stream(model_name, messages, max_tokens=512, temperature=0.7,
370
+ think_mode=False, deep_think=False, search_context=None):
371
+ from exocore_model import build_chat_prompt, IM_END, EOT
372
+
373
+ mc = load_exocore_model(model_name)
374
+ tok = mc["tokenizer"]
375
+ model = mc["model"]
376
+ cfg = mc["config"]
377
+ max_pos = min(cfg.get("max_position_embeddings", 8192), 4096)
378
+
379
+ def sample_next(ids):
380
+ ctx = ids[:, -min(ids.shape[1], max_pos):]
381
+ with torch.no_grad():
382
+ logits, _ = model(ctx)
383
+ logits = logits[:, -1, :].float() / max(temperature, 1e-5)
384
+ v, _ = torch.topk(logits, min(50, logits.size(-1)))
385
+ logits[logits < v[:, [-1]]] = float("-inf")
386
+ probs = torch.softmax(logits, dim=-1)
387
+ return torch.multinomial(probs, 1)
388
+
389
+ def encode(prompt):
390
+ enc = tok.encode(prompt)
391
+ return torch.tensor([enc.ids], dtype=torch.long)
392
+
393
+ def collect_tokens(ids, max_t, stop_ids=(IM_END, EOT)):
394
+ out_ids = []
395
+ for _ in range(max_t):
396
+ nxt = sample_next(ids)
397
+ tid = nxt.item()
398
+ ids = torch.cat([ids, nxt], dim=1)
399
+ if tid in stop_ids:
400
+ break
401
+ out_ids.append(tid)
402
+ return tok.decode(out_ids), ids
403
+
404
+ if deep_think:
405
+ think1_prompt = build_chat_prompt(messages, think=True, search_context=search_context)
406
+ think1_raw, _ = collect_tokens(encode(think1_prompt), min(max_tokens * 2, 1024))
407
+ think1 = think1_raw.replace("<think>", "").replace("</think>", "").strip()
408
+
409
+ think2_prompt = build_chat_prompt(messages, think=True, prior_thinking=think1, search_context=search_context)
410
+ think2_raw, _ = collect_tokens(encode(think2_prompt), min(max_tokens, 512))
411
+ think2 = think2_raw.replace("<think>", "").replace("</think>", "").strip()
412
+
413
+ combined = f"[Pass 1]\n{think1}\n\n[Pass 2]\n{think2}"
414
+ yield ("think_block", combined)
415
+
416
+ answer_prompt = build_chat_prompt(messages, think=False, prior_thinking=combined, search_context=search_context)
417
+ ids = encode(answer_prompt)
418
+ for _ in range(max_tokens):
419
+ nxt = sample_next(ids)
420
+ tid = nxt.item()
421
+ ids = torch.cat([ids, nxt], dim=1)
422
+ if tid in (IM_END, EOT):
423
+ break
424
+ yield ("token", tok.decode([tid]))
425
+
426
  else:
427
+ prompt = build_chat_prompt(messages, think=think_mode, search_context=search_context)
428
+ ids = encode(prompt)
429
+ buf = ""
430
+ in_think = False
431
+ think_done = not think_mode
432
+ think_buf = ""
433
+ think_sent = False
434
+
435
+ for _ in range(max_tokens):
436
+ nxt = sample_next(ids)
437
+ tid = nxt.item()
438
+ ids = torch.cat([ids, nxt], dim=1)
439
+ if tid in (IM_END, EOT):
440
+ break
441
+
442
+ buf += tok.decode([tid])
443
+
444
+ if not think_done:
445
+ if not in_think and "<think>" in buf:
446
+ buf = buf.split("<think>", 1)[1]
447
+ in_think = True
448
+ if in_think:
449
+ if "</think>" in buf:
450
+ ttext, buf = buf.split("</think>", 1)
451
+ think_buf += ttext
452
+ think_done = True
453
+ in_think = False
454
+ if not think_sent:
455
+ yield ("think_block", think_buf.strip())
456
+ think_sent = True
457
+ else:
458
+ think_buf += buf
459
+ buf = ""
460
+ continue
461
+
462
+ if not in_think and buf:
463
+ yield ("token", buf)
464
+ buf = ""
465
+
466
+ if buf and not in_think:
467
+ yield ("token", buf)