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:
Dani 2025-04-29 23:04:53 -04:00
parent f41d14075e
commit a8b3129806
12 changed files with 205 additions and 288 deletions

View File

@ -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")

View File

@ -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
View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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):

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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")