Redid the dashboard home page into more of a status page.
Fixed the weird desync/threading issue that was stopping ruby from working.
This commit is contained in:
parent
f41d14075e
commit
a8b3129806
@ -59,28 +59,34 @@ def update_next_cycle(seconds):
|
||||
next_cycle_time = time.time() + seconds
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
dreams = load_dreams()
|
||||
top_dreams = dreams[:5]
|
||||
memory_size = len(load_context())
|
||||
loss_data = load_loss_data()
|
||||
def get_status_summary():
|
||||
progress = load_progress()
|
||||
books = get_books()
|
||||
current_book = books[0] if books else None
|
||||
current_line = progress.get(current_book, 0)
|
||||
next_cycle = get_time_until_next_action()
|
||||
next_action_label = get_next_action_label()
|
||||
total_lines = 1
|
||||
if current_book:
|
||||
with open(f"books/{current_book}", "r", encoding="utf-8") as f:
|
||||
total_lines = len(f.readlines())
|
||||
|
||||
return render_template("index.html",
|
||||
vocab_size=get_vocab_size(),
|
||||
top_dreams=top_dreams,
|
||||
memory_size=memory_size,
|
||||
loss_data=loss_data,
|
||||
current_book=current_book,
|
||||
current_line=current_line,
|
||||
next_cycle=next_cycle,
|
||||
next_action_label=next_action_label)
|
||||
return {
|
||||
"current_book": current_book,
|
||||
"current_line": current_line,
|
||||
"percent_done": round((current_line / total_lines) * 100, 2),
|
||||
"memory_size": len(load_context()),
|
||||
"vocab_size": get_vocab_size(),
|
||||
"brainmap_size": len(get_brainmap()),
|
||||
"journal_count": len(read_journal_entries()),
|
||||
"dream": load_dreams()[-1] if load_dreams() else None,
|
||||
"next_action_label": get_next_action_label(),
|
||||
"next_cycle": get_time_until_next_action()
|
||||
}
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
status = get_status_summary()
|
||||
return render_template("index.html", status=status)
|
||||
|
||||
|
||||
@app.route("/growth")
|
||||
@ -131,7 +137,10 @@ def journal():
|
||||
@app.route("/concepts")
|
||||
def concepts():
|
||||
clusters = cluster_vocab(n_clusters=10)
|
||||
return render_template("concepts.html", clusters={i: cluster for i, cluster in enumerate(clusters)})
|
||||
return render_template("concepts.html",
|
||||
clusters={i: cluster for i,
|
||||
cluster in enumerate(clusters)
|
||||
})
|
||||
|
||||
|
||||
@app.route("/dreams")
|
||||
|
@ -2,48 +2,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="30">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<title>Ruby's Dashboard</title>
|
||||
<meta http-equiv="refresh" content="15">
|
||||
<title>Ruby Status Dashboard</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0f0f0f;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.section {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
h1, h2 {
|
||||
color: #ffd700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
p {
|
||||
margin: 5px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
li {
|
||||
background: #262626;
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.nav {
|
||||
background-color: #1e1e1e;
|
||||
padding: 10px;
|
||||
@ -54,123 +22,87 @@
|
||||
margin-right: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.preview-box {
|
||||
background: #262626;
|
||||
padding: 15px;
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #ffd700;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.section {
|
||||
background-color: #1a1a1a;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.section h2 {
|
||||
margin-top: 0;
|
||||
color: #f0f0f0;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.kv {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.kv strong {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
color: #ccc;
|
||||
}
|
||||
.dream-box {
|
||||
background-color: #262626;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nav">
|
||||
<a href="/">🏠 Home</a>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/">📊 Dashboard</a>
|
||||
<a href="/journal">📓 Journal</a>
|
||||
<a href="/concepts">🧠 Concepts</a>
|
||||
<a href="/brainmap">🕸️ Brain Map</a>
|
||||
<a href="/growth">📈 Growth</a>
|
||||
<a href="/dreams">💬 Dreams</a>
|
||||
<a href="/dreams">🌃 Dreams</a>
|
||||
<a href="/health">❤️ Health</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1 style="text-align: center;">Ruby is Running 🧠</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>⏳ Next Cycle</h2>
|
||||
<p><strong>Next:</strong> {{ next_action_label }}</p>
|
||||
<p id="countdown">{{ next_cycle }} seconds</p>
|
||||
</div>
|
||||
<script>
|
||||
function updateCountdown() {
|
||||
var countdown = document.getElementById("countdown");
|
||||
var seconds = parseInt(countdown.innerText.split(" ")[0]);
|
||||
if (seconds > 0) {
|
||||
seconds -= 1;
|
||||
countdown.innerText = seconds + " seconds";
|
||||
}
|
||||
}
|
||||
setInterval(updateCountdown, 1000);
|
||||
</script>
|
||||
|
||||
<div class="section">
|
||||
<h2>🧠 Brain Stats</h2>
|
||||
<p><strong>Vocabulary Size:</strong> {{ vocab_size }}</p>
|
||||
<p><strong>Memory Entries:</strong> {{ memory_size }}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📖 Current Book Progress</h2>
|
||||
<p><strong>Currently Reading:</strong> {{ current_book }}</p>
|
||||
<p><strong>Line:</strong> {{ current_line }}</p>
|
||||
<div class="preview-box">
|
||||
{{ current_passage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🏆 Highest Scoring Dreams</h2>
|
||||
<ul>
|
||||
{% for dream in top_dreams %}
|
||||
<li><strong>{{ dream.score }}</strong> | {{ dream.sentence }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📉 Recent Loss</h2>
|
||||
<canvas id="lossChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ctxLoss = document.getElementById('lossChart').getContext('2d');
|
||||
|
||||
const lossData = {
|
||||
labels: [
|
||||
{% for entry in loss_data[-50:] %}
|
||||
"{{ loop.index0 }}",
|
||||
{% endfor %}
|
||||
],
|
||||
datasets: [{
|
||||
label: 'Loss',
|
||||
data: [
|
||||
{% for entry in loss_data[-50:] %}
|
||||
{{ entry }},
|
||||
{% endfor %}
|
||||
],
|
||||
fill: false,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
tension: 0.2
|
||||
}]
|
||||
};
|
||||
|
||||
const lossChart = new Chart(ctxLoss, {
|
||||
type: 'line',
|
||||
data: lossData,
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Loss Value'
|
||||
},
|
||||
beginAtZero: false
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<h1>🧠 Ruby System Status</h1>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<div class="section">
|
||||
<h2>🔄 Current Activity</h2>
|
||||
<div class="kv"><strong>Action:</strong> {{ status.next_action_label }}</div>
|
||||
<div class="kv"><strong>Next in:</strong> {{ status.next_cycle }} sec</div>
|
||||
<div class="kv"><strong>Reading:</strong> {{ status.current_book or "None" }}</div>
|
||||
<div class="kv"><strong>Line:</strong> {{ status.current_line }} ({{ status.percent_done }}%)</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📊 System Stats</h2>
|
||||
<div class="kv"><strong>Vocabulary:</strong> {{ status.vocab_size }}</div>
|
||||
<div class="kv"><strong>Memory:</strong> {{ status.memory_size }}</div>
|
||||
<div class="kv"><strong>Brain Map:</strong> {{ status.brainmap_size }}</div>
|
||||
<div class="kv"><strong>Journal:</strong> {{ status.journal_count }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top: 20px;">
|
||||
<h2>💤 Latest Dream</h2>
|
||||
{% if status.dream %}
|
||||
<div><strong>Score:</strong> {{ status.dream.score }}</div>
|
||||
<div class="dream-box">{{ status.dream.sentence }}</div>
|
||||
{% else %}
|
||||
<p>No dreams yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
43
main.py
43
main.py
@ -29,6 +29,13 @@ empty_response_counter = 0
|
||||
async def on_ready():
|
||||
print(f"Ruby is online as {client.user}.")
|
||||
|
||||
# ✅ Start async loops on Discord's own event loop
|
||||
client.loop.create_task(read_books_forever())
|
||||
client.loop.create_task(dream_replay_loop())
|
||||
client.loop.create_task(background_cleanup_loop())
|
||||
client.loop.create_task(rehearsal_loop())
|
||||
client.loop.create_task(memory_reweaver_loop())
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
@ -40,16 +47,16 @@ async def on_message(message):
|
||||
if not message.content.strip():
|
||||
return
|
||||
|
||||
train_on_message(message.content, source="user")
|
||||
await train_on_message(message.content, source="user")
|
||||
response = generate_response()
|
||||
|
||||
if not response.strip():
|
||||
empty_response_counter += 1
|
||||
if empty_response_counter % 10 == 0: # only every 10 failures
|
||||
if empty_response_counter % 10 == 0:
|
||||
print(f"[Brain] Skipped {empty_response_counter} empty replies so far.")
|
||||
return
|
||||
|
||||
empty_response_counter = 0 # reset counter when Ruby replies
|
||||
empty_response_counter = 0
|
||||
await message.channel.send(response)
|
||||
|
||||
|
||||
@ -57,40 +64,26 @@ async def background_cleanup_loop():
|
||||
while True:
|
||||
full_cleanup()
|
||||
set_next_action(300, "Cleaning up")
|
||||
await asyncio.sleep(300) # 5 minutes
|
||||
await asyncio.sleep(300)
|
||||
|
||||
|
||||
async def dream_replay_loop():
|
||||
while True:
|
||||
replay_dreams()
|
||||
await replay_dreams()
|
||||
set_next_action(90, "Dreaming new dreams")
|
||||
await asyncio.sleep(90) # Replay every 15 minutes
|
||||
daydream()
|
||||
await asyncio.sleep(90)
|
||||
await daydream()
|
||||
|
||||
|
||||
async def rehearsal_loop():
|
||||
while True:
|
||||
simulate_conversation()
|
||||
await simulate_conversation()
|
||||
set_next_action(120, "Practicing Conversations")
|
||||
await asyncio.sleep(120) # Every 20 minutes
|
||||
|
||||
|
||||
# Start Ruby's Brain Loops in a separate thread
|
||||
def start_brain_loops():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
loop.create_task(read_books_forever())
|
||||
loop.create_task(dream_replay_loop())
|
||||
loop.create_task(background_cleanup_loop())
|
||||
loop.create_task(rehearsal_loop())
|
||||
loop.create_task(memory_reweaver_loop())
|
||||
|
||||
loop.run_forever()
|
||||
await asyncio.sleep(120)
|
||||
|
||||
|
||||
# ✅ Launch dashboard in background thread
|
||||
threading.Thread(target=run_dashboard, daemon=True).start()
|
||||
threading.Thread(target=start_brain_loops, daemon=True).start()
|
||||
|
||||
# Launch Discord bot (blocking)
|
||||
# ✅ Launch Discord bot (this owns the event loop now)
|
||||
client.run(TOKEN)
|
||||
|
@ -4,22 +4,25 @@ from model.tokenizer import Tokenizer
|
||||
|
||||
tokenizer = Tokenizer()
|
||||
|
||||
SPECIAL_TOKENS = {"<pad>", "<unk>", "<start>", "<end>", "<sep>"}
|
||||
|
||||
|
||||
def cluster_vocab(n_clusters=10):
|
||||
vocab_items = list(tokenizer.vocab.items())
|
||||
vocab_items = [(word, idx) for word, idx in tokenizer.vocab.items() if word not in SPECIAL_TOKENS]
|
||||
|
||||
if not vocab_items:
|
||||
return [] # If vocab is empty, just return empty clusters safely
|
||||
if len(vocab_items) < 2:
|
||||
return [] # Not enough real words to cluster
|
||||
|
||||
words, ids = zip(*vocab_items)
|
||||
ids = torch.tensor(ids, dtype=torch.float32).unsqueeze(1)
|
||||
|
||||
kmeans = KMeans(n_clusters=min(n_clusters, len(words)))
|
||||
labels = kmeans.fit_predict(ids)
|
||||
# Use 1D embedding: you can expand this to real model vectors later
|
||||
vectors = torch.eye(len(words), dtype=torch.float32) # fake embeddings
|
||||
|
||||
kmeans = KMeans(n_clusters=min(n_clusters, len(words)), n_init="auto")
|
||||
labels = kmeans.fit_predict(vectors)
|
||||
|
||||
clusters = [[] for _ in range(max(labels) + 1)]
|
||||
for word, label in zip(words, labels):
|
||||
clusters[label].append(word)
|
||||
|
||||
return clusters
|
||||
|
||||
|
@ -11,22 +11,18 @@ from context.context import load_context
|
||||
recent_dreams = []
|
||||
|
||||
|
||||
def daydream():
|
||||
async def daydream():
|
||||
model.eval()
|
||||
max_token_id = model.head.out_features - 1
|
||||
seed = torch.randint(0, max_token_id + 1, (1, 1), device=DEVICE)
|
||||
dream = []
|
||||
max_token_id = model.head.out_features - 1
|
||||
|
||||
for _ in range(12):
|
||||
out = model(seed)
|
||||
logits = out[:, -1, :]
|
||||
probs = F.softmax(logits, dim=-1)
|
||||
token = torch.multinomial(probs, num_samples=1)
|
||||
|
||||
# CLAMP the token
|
||||
token = torch.clamp(token, max=max_token_id)
|
||||
|
||||
dream.append(token.item())
|
||||
seed = torch.cat([seed, token], dim=1)
|
||||
|
||||
@ -36,15 +32,14 @@ def daydream():
|
||||
if score > 0.5:
|
||||
save_dream(sentence, score)
|
||||
record_to_journal(sentence)
|
||||
train_on_message(sentence)
|
||||
await train_on_message(sentence)
|
||||
|
||||
if len(recent_dreams) > 10:
|
||||
recent_dreams.pop(0)
|
||||
|
||||
|
||||
def replay_dreams():
|
||||
expand_model_if_needed()
|
||||
|
||||
async def replay_dreams():
|
||||
await expand_model_if_needed()
|
||||
dreams = load_dreams()
|
||||
context = load_context()
|
||||
|
||||
@ -54,11 +49,10 @@ def replay_dreams():
|
||||
selected_dreams = random.sample(dreams, min(len(dreams), 5))
|
||||
selected_contexts = random.sample(context, min(len(context), 5))
|
||||
|
||||
# Mix dreams and past contexts into a chaotic dream
|
||||
all_sources = [d["sentence"] for d in selected_dreams] + [c["text"] for c in selected_contexts]
|
||||
random.shuffle(all_sources)
|
||||
|
||||
mixed_sentence = " ".join(random.sample(all_sources, min(len(all_sources), 3)))
|
||||
|
||||
if mixed_sentence:
|
||||
train_on_message(mixed_sentence, source="dream")
|
||||
await train_on_message(mixed_sentence, source="dream")
|
||||
|
@ -7,8 +7,12 @@ DREAM_LOG_PATH = "data/memory/dreams.json"
|
||||
def load_dreams():
|
||||
if not os.path.exists(DREAM_LOG_PATH):
|
||||
return []
|
||||
try:
|
||||
with open(DREAM_LOG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
print("[Dreams] Failed to parse dreams.json.")
|
||||
return []
|
||||
|
||||
|
||||
def save_dream(sentence: str, score: float):
|
||||
|
@ -1,43 +1,37 @@
|
||||
import torch
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
from model.tokenizer import Tokenizer
|
||||
from model.brain_state import save_model, DEVICE, model, optimizer
|
||||
|
||||
tokenizer = Tokenizer()
|
||||
expand_lock = threading.Lock()
|
||||
expand_lock = asyncio.Lock()
|
||||
_last_expansion_time = 0
|
||||
|
||||
|
||||
def expand_model_if_needed():
|
||||
async def expand_model_if_needed():
|
||||
global _last_expansion_time
|
||||
|
||||
with expand_lock:
|
||||
# Check if expansion is actually needed
|
||||
async with expand_lock:
|
||||
needed_vocab_size = tokenizer.next_id
|
||||
current_vocab_size = model.head.out_features
|
||||
|
||||
if needed_vocab_size <= current_vocab_size:
|
||||
return # ✅ No expansion needed
|
||||
return
|
||||
|
||||
# print(f"[Expand] Expanding vocabulary: {current_vocab_size} -> {needed_vocab_size}")
|
||||
print(f"[Expand] Expanding vocabulary: {current_vocab_size} -> {needed_vocab_size}")
|
||||
|
||||
# Expand the head layer safely without rebuilding everything
|
||||
old_head_weight = model.head.weight.data
|
||||
old_out_features = old_head_weight.size(0)
|
||||
in_features = model.head.in_features
|
||||
|
||||
new_head = torch.nn.Linear(in_features, needed_vocab_size, bias=False)
|
||||
new_head = new_head.to(DEVICE)
|
||||
new_head = torch.nn.Linear(in_features, needed_vocab_size, bias=False).to(DEVICE)
|
||||
|
||||
# Copy old weights into the new head
|
||||
with torch.no_grad():
|
||||
new_head.weight[:old_out_features] = old_head_weight
|
||||
|
||||
model.head = new_head
|
||||
|
||||
# Rebuild optimizer and scheduler
|
||||
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1000, gamma=0.95)
|
||||
torch.optim.lr_scheduler.StepLR(optimizer, step_size=1000, gamma=0.95)
|
||||
|
||||
_last_expansion_time = time.time()
|
||||
save_model()
|
||||
|
@ -26,8 +26,11 @@ def read_journal_entries():
|
||||
if not os.path.exists(JOURNAL_PATH):
|
||||
return []
|
||||
with open(JOURNAL_PATH, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
return [line.split("|", 1)[-1].strip() for line in lines if "|" in line]
|
||||
try:
|
||||
journal = json.load(f)
|
||||
return [entry.get("text", "") for entry in journal if isinstance(entry, dict)]
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def sample_journal_entries(n=5):
|
||||
|
@ -1,30 +1,27 @@
|
||||
import torch
|
||||
from model.brain import model, tokenizer, DEVICE
|
||||
from model.trainer import train_on_message
|
||||
from model.dynamic_expand import expand_model_if_needed
|
||||
from model.trainer import train_on_message
|
||||
|
||||
|
||||
def simulate_conversation():
|
||||
expand_model_if_needed()
|
||||
|
||||
async def simulate_conversation():
|
||||
await expand_model_if_needed()
|
||||
model.eval()
|
||||
|
||||
max_token_id = model.head.out_features - 1
|
||||
if max_token_id < 1:
|
||||
return # Safeguard if model is still too small
|
||||
return
|
||||
|
||||
seed = torch.randint(0, max_token_id + 1, (1, 5), device=DEVICE)
|
||||
seed = seed[:, -128:] # Clamp sequence length
|
||||
seed = seed[:, -128:]
|
||||
|
||||
output = model(seed)
|
||||
|
||||
preds = torch.argmax(output, dim=-1).squeeze().tolist()
|
||||
if isinstance(preds, int):
|
||||
preds = [preds]
|
||||
|
||||
# 🛡 Clamp predictions too
|
||||
preds = [min(max(p, 0), max_token_id) for p in preds]
|
||||
|
||||
text = tokenizer.detokenize(preds)
|
||||
|
||||
if text and len(text.split()) >= 3:
|
||||
train_on_message(text)
|
||||
await train_on_message(text)
|
||||
|
@ -8,7 +8,7 @@ from model.dynamic_expand import expand_model_if_needed
|
||||
async def memory_reweaver_loop():
|
||||
while True:
|
||||
await asyncio.sleep(600) # every 10 minutes
|
||||
expand_model_if_needed()
|
||||
await expand_model_if_needed()
|
||||
|
||||
context = load_context()
|
||||
if not context:
|
||||
@ -18,4 +18,4 @@ async def memory_reweaver_loop():
|
||||
combined_text = " ".join([s["text"] for s in selected])
|
||||
|
||||
if combined_text:
|
||||
train_on_message(combined_text, source="reweaver")
|
||||
await train_on_message(combined_text, source="reweaver")
|
||||
|
@ -1,6 +1,6 @@
|
||||
import torch
|
||||
import time
|
||||
from model.dynamic_expand import expand_model_if_needed, _last_expansion_time, expand_lock
|
||||
from model.dynamic_expand import expand_model_if_needed, _last_expansion_time
|
||||
from model.brain_state import model, tokenizer, DEVICE, loss_fn, optimizer, scheduler
|
||||
from model.brainmap import add_to_brainmap
|
||||
from model.journal import record_to_journal
|
||||
@ -20,37 +20,30 @@ def log_loss(value: float):
|
||||
f.write(f"{time.time()},{round(value, 4)}\n")
|
||||
|
||||
|
||||
def train_on_message(text: str, source: str = "user"):
|
||||
expand_model_if_needed()
|
||||
async def train_on_message(text: str, source: str = "user"):
|
||||
await expand_model_if_needed()
|
||||
|
||||
now = time.time()
|
||||
if now - _last_expansion_time < 5:
|
||||
print("[Trainer] Skipping to stabilize after expansion.")
|
||||
return
|
||||
|
||||
if not expand_lock.acquire(timeout=0.5):
|
||||
print("[Trainer] Skipped training due to active expansion.")
|
||||
return
|
||||
|
||||
try:
|
||||
model.train()
|
||||
context_texts = get_recent_context(10)
|
||||
|
||||
# Augment the input with recent context
|
||||
augmented_text = "<start> " + " ".join(context_texts + [text]) + " <end>"
|
||||
|
||||
tokens = tokenizer.tokenize(augmented_text)
|
||||
|
||||
if len(tokens) < 2:
|
||||
print("[Trainer] Message too short after cleaning.")
|
||||
return
|
||||
|
||||
# Clamp any token IDs beyond the model's output size
|
||||
max_token_id = model.head.out_features - 1
|
||||
if tokenizer.next_id > model.head.out_features:
|
||||
expand_model_if_needed()
|
||||
tokens = [t if t <= max_token_id else max_token_id for t in tokens]
|
||||
tokens = tokens[:128] # Hard clamp input length
|
||||
tokens = [max(0, min(t, max_token_id)) for t in tokens][:128]
|
||||
|
||||
for t in tokens:
|
||||
if t > max_token_id or t < 0:
|
||||
print(f"[Trainer] Invalid token ID {t} (max={max_token_id})")
|
||||
return
|
||||
|
||||
if len(tokens) < 2:
|
||||
print("[Trainer] Message too short after clamping.")
|
||||
@ -70,11 +63,9 @@ def train_on_message(text: str, source: str = "user"):
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
|
||||
# Update brainmap and context
|
||||
add_to_brainmap(augmented_text.split())
|
||||
add_to_context(text, source=source)
|
||||
|
||||
# Log training success to journal
|
||||
record_to_journal({
|
||||
"timestamp": time.time(),
|
||||
"source": source,
|
||||
@ -82,6 +73,3 @@ def train_on_message(text: str, source: str = "user"):
|
||||
"loss": round(loss.item(), 4),
|
||||
"vocab_size": len(tokenizer.vocab)
|
||||
})
|
||||
|
||||
finally:
|
||||
expand_lock.release()
|
||||
|
@ -76,7 +76,7 @@ async def read_books_forever():
|
||||
paragraph += " " + line
|
||||
|
||||
if line[-1] in END_PUNCTUATION and len(paragraph) > PARAGRAPH_MIN_LENGTH:
|
||||
train_on_message(paragraph.strip(), source="book")
|
||||
await train_on_message(paragraph.strip(), source="book")
|
||||
paragraph = ""
|
||||
await asyncio.sleep(READ_DELAY)
|
||||
set_next_action(READ_DELAY, "Reading")
|
||||
@ -87,7 +87,7 @@ async def read_books_forever():
|
||||
|
||||
if paragraph.strip():
|
||||
if len(paragraph) > PARAGRAPH_MIN_LENGTH:
|
||||
train_on_message(paragraph.strip(), source="book")
|
||||
await train_on_message(paragraph.strip(), source="book")
|
||||
await asyncio.sleep(READ_DELAY)
|
||||
set_next_action(READ_DELAY, "Reading")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user