ExocoreV1: multi-thinking, web search, auto-load, loss chart, architecture tree, no system prompt
Browse files- exocore_model.py +165 -0
- main.py +238 -257
- replit.md +91 -91
- templates/index.html +565 -0
- templates/train.html +401 -0
- 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
|
|
|
|
|
|
|
| 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
|
| 16 |
loaded_model_path = None
|
| 17 |
-
loaded_model_type = None
|
| 18 |
-
chat_history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 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 |
-
|
| 35 |
-
|
| 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 == "
|
| 67 |
-
mc = tr.
|
| 68 |
-
loaded_model
|
| 69 |
loaded_model_path = model_name
|
| 70 |
-
loaded_model_type = "
|
| 71 |
chat_history = []
|
| 72 |
-
return jsonify({"success": True, "model": model_name, "type": "
|
| 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
|
| 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
|
| 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
|
| 117 |
user_message = data.get("message", "").strip()
|
| 118 |
-
max_tokens
|
| 119 |
-
temperature
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
if not user_message:
|
| 122 |
return jsonify({"error": "Empty message"}), 400
|
| 123 |
|
| 124 |
-
|
| 125 |
-
think_mode = (mode == "thinking")
|
| 126 |
-
|
| 127 |
-
if loaded_model_type == "hf":
|
| 128 |
messages = list(chat_history) + [{"role": "user", "content": user_message}]
|
| 129 |
-
|
|
|
|
| 130 |
full_reply = ""
|
| 131 |
-
think_sent = False
|
| 132 |
try:
|
| 133 |
-
for
|
| 134 |
-
loaded_model_path, messages, max_tokens, temperature,
|
|
|
|
|
|
|
| 135 |
):
|
| 136 |
-
if
|
| 137 |
yield f"data: {json.dumps({'think_block': payload})}\n\n"
|
| 138 |
-
|
| 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",
|
| 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 |
-
|
|
|
|
| 148 |
|
| 149 |
if loaded_model_type == "pt":
|
| 150 |
-
|
| 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 |
-
|
| 176 |
-
|
| 177 |
-
|
| 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",
|
| 188 |
-
chat_history.append({"role": "assistant", "content":
|
| 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(
|
| 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 |
-
|
| 197 |
|
| 198 |
-
def
|
| 199 |
-
|
| 200 |
try:
|
| 201 |
-
stream = loaded_model.create_chat_completion(
|
| 202 |
-
|
| 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 |
-
|
| 211 |
-
|
| 212 |
-
yield f"data: {json.dumps({'token':
|
| 213 |
-
chat_history.append({"role": "assistant", "content":
|
| 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":
|
| 232 |
-
"model_name":
|
| 233 |
-
"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
|
| 248 |
-
text
|
| 249 |
-
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":
|
| 300 |
-
"
|
| 301 |
-
"file_size_mb":
|
| 302 |
-
"modified":
|
| 303 |
-
"total_params":
|
| 304 |
-
"
|
| 305 |
-
"
|
| 306 |
-
"
|
| 307 |
-
"
|
| 308 |
-
"
|
| 309 |
-
"
|
| 310 |
-
"
|
| 311 |
-
"
|
| 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":
|
| 348 |
-
"
|
| 349 |
-
"file_size_mb":
|
| 350 |
-
"modified":
|
| 351 |
-
"
|
| 352 |
-
"
|
| 353 |
-
"
|
| 354 |
-
"
|
| 355 |
-
"
|
| 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/
|
| 379 |
-
def
|
| 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
|
| 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.
|
| 423 |
return jsonify({"error": "Not found"}), 404
|
| 424 |
try:
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 487 |
-
model_name = data.get("model_name",
|
| 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 |
-
"
|
| 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 |
-
#
|
| 2 |
-
|
| 3 |
-
A
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
- `
|
| 22 |
-
-
|
| 23 |
-
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
-
|
| 83 |
-
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
-
|
|
|
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
| 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 |
-
|
| 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
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
else:
|
| 782 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|