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
|
next_cycle_time = time.time() + seconds
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
def get_status_summary():
|
||||||
def index():
|
|
||||||
dreams = load_dreams()
|
|
||||||
top_dreams = dreams[:5]
|
|
||||||
memory_size = len(load_context())
|
|
||||||
loss_data = load_loss_data()
|
|
||||||
progress = load_progress()
|
progress = load_progress()
|
||||||
books = get_books()
|
books = get_books()
|
||||||
current_book = books[0] if books else None
|
current_book = books[0] if books else None
|
||||||
current_line = progress.get(current_book, 0)
|
current_line = progress.get(current_book, 0)
|
||||||
next_cycle = get_time_until_next_action()
|
total_lines = 1
|
||||||
next_action_label = get_next_action_label()
|
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",
|
return {
|
||||||
vocab_size=get_vocab_size(),
|
"current_book": current_book,
|
||||||
top_dreams=top_dreams,
|
"current_line": current_line,
|
||||||
memory_size=memory_size,
|
"percent_done": round((current_line / total_lines) * 100, 2),
|
||||||
loss_data=loss_data,
|
"memory_size": len(load_context()),
|
||||||
current_book=current_book,
|
"vocab_size": get_vocab_size(),
|
||||||
current_line=current_line,
|
"brainmap_size": len(get_brainmap()),
|
||||||
next_cycle=next_cycle,
|
"journal_count": len(read_journal_entries()),
|
||||||
next_action_label=next_action_label)
|
"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")
|
@app.route("/growth")
|
||||||
@ -131,7 +137,10 @@ def journal():
|
|||||||
@app.route("/concepts")
|
@app.route("/concepts")
|
||||||
def concepts():
|
def concepts():
|
||||||
clusters = cluster_vocab(n_clusters=10)
|
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")
|
@app.route("/dreams")
|
||||||
|
@ -2,48 +2,16 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="30">
|
<meta http-equiv="refresh" content="15">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<title>Ruby Status Dashboard</title>
|
||||||
<title>Ruby's Dashboard</title>
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #0f0f0f;
|
background-color: #0f0f0f;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 20px;
|
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 {
|
.nav {
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -54,123 +22,87 @@
|
|||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.preview-box {
|
h1 {
|
||||||
background: #262626;
|
text-align: center;
|
||||||
padding: 15px;
|
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;
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 14px;
|
font-style: italic;
|
||||||
max-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="nav">
|
|
||||||
<a href="/">🏠 Home</a>
|
<div class="nav">
|
||||||
|
<a href="/">📊 Dashboard</a>
|
||||||
<a href="/journal">📓 Journal</a>
|
<a href="/journal">📓 Journal</a>
|
||||||
<a href="/concepts">🧠 Concepts</a>
|
<a href="/concepts">🧠 Concepts</a>
|
||||||
<a href="/brainmap">🕸️ Brain Map</a>
|
<a href="/brainmap">🕸️ Brain Map</a>
|
||||||
<a href="/growth">📈 Growth</a>
|
<a href="/growth">📈 Growth</a>
|
||||||
<a href="/dreams">💬 Dreams</a>
|
<a href="/dreams">🌃 Dreams</a>
|
||||||
<a href="/health">❤️ Health</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>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
43
main.py
43
main.py
@ -29,6 +29,13 @@ empty_response_counter = 0
|
|||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f"Ruby is online as {client.user}.")
|
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
|
@client.event
|
||||||
async def on_message(message):
|
async def on_message(message):
|
||||||
@ -40,16 +47,16 @@ async def on_message(message):
|
|||||||
if not message.content.strip():
|
if not message.content.strip():
|
||||||
return
|
return
|
||||||
|
|
||||||
train_on_message(message.content, source="user")
|
await train_on_message(message.content, source="user")
|
||||||
response = generate_response()
|
response = generate_response()
|
||||||
|
|
||||||
if not response.strip():
|
if not response.strip():
|
||||||
empty_response_counter += 1
|
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.")
|
print(f"[Brain] Skipped {empty_response_counter} empty replies so far.")
|
||||||
return
|
return
|
||||||
|
|
||||||
empty_response_counter = 0 # reset counter when Ruby replies
|
empty_response_counter = 0
|
||||||
await message.channel.send(response)
|
await message.channel.send(response)
|
||||||
|
|
||||||
|
|
||||||
@ -57,40 +64,26 @@ async def background_cleanup_loop():
|
|||||||
while True:
|
while True:
|
||||||
full_cleanup()
|
full_cleanup()
|
||||||
set_next_action(300, "Cleaning up")
|
set_next_action(300, "Cleaning up")
|
||||||
await asyncio.sleep(300) # 5 minutes
|
await asyncio.sleep(300)
|
||||||
|
|
||||||
|
|
||||||
async def dream_replay_loop():
|
async def dream_replay_loop():
|
||||||
while True:
|
while True:
|
||||||
replay_dreams()
|
await replay_dreams()
|
||||||
set_next_action(90, "Dreaming new dreams")
|
set_next_action(90, "Dreaming new dreams")
|
||||||
await asyncio.sleep(90) # Replay every 15 minutes
|
await asyncio.sleep(90)
|
||||||
daydream()
|
await daydream()
|
||||||
|
|
||||||
|
|
||||||
async def rehearsal_loop():
|
async def rehearsal_loop():
|
||||||
while True:
|
while True:
|
||||||
simulate_conversation()
|
await simulate_conversation()
|
||||||
set_next_action(120, "Practicing Conversations")
|
set_next_action(120, "Practicing Conversations")
|
||||||
await asyncio.sleep(120) # Every 20 minutes
|
await asyncio.sleep(120)
|
||||||
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
|
|
||||||
|
# ✅ Launch dashboard in background thread
|
||||||
threading.Thread(target=run_dashboard, daemon=True).start()
|
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)
|
client.run(TOKEN)
|
||||||
|
@ -4,22 +4,25 @@ from model.tokenizer import Tokenizer
|
|||||||
|
|
||||||
tokenizer = Tokenizer()
|
tokenizer = Tokenizer()
|
||||||
|
|
||||||
|
SPECIAL_TOKENS = {"<pad>", "<unk>", "<start>", "<end>", "<sep>"}
|
||||||
|
|
||||||
|
|
||||||
def cluster_vocab(n_clusters=10):
|
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:
|
if len(vocab_items) < 2:
|
||||||
return [] # If vocab is empty, just return empty clusters safely
|
return [] # Not enough real words to cluster
|
||||||
|
|
||||||
words, ids = zip(*vocab_items)
|
words, ids = zip(*vocab_items)
|
||||||
ids = torch.tensor(ids, dtype=torch.float32).unsqueeze(1)
|
|
||||||
|
|
||||||
kmeans = KMeans(n_clusters=min(n_clusters, len(words)))
|
# Use 1D embedding: you can expand this to real model vectors later
|
||||||
labels = kmeans.fit_predict(ids)
|
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)]
|
clusters = [[] for _ in range(max(labels) + 1)]
|
||||||
for word, label in zip(words, labels):
|
for word, label in zip(words, labels):
|
||||||
clusters[label].append(word)
|
clusters[label].append(word)
|
||||||
|
|
||||||
return clusters
|
return clusters
|
||||||
|
|
||||||
|
@ -11,22 +11,18 @@ from context.context import load_context
|
|||||||
recent_dreams = []
|
recent_dreams = []
|
||||||
|
|
||||||
|
|
||||||
def daydream():
|
async def daydream():
|
||||||
model.eval()
|
model.eval()
|
||||||
max_token_id = model.head.out_features - 1
|
max_token_id = model.head.out_features - 1
|
||||||
seed = torch.randint(0, max_token_id + 1, (1, 1), device=DEVICE)
|
seed = torch.randint(0, max_token_id + 1, (1, 1), device=DEVICE)
|
||||||
dream = []
|
dream = []
|
||||||
max_token_id = model.head.out_features - 1
|
|
||||||
|
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
out = model(seed)
|
out = model(seed)
|
||||||
logits = out[:, -1, :]
|
logits = out[:, -1, :]
|
||||||
probs = F.softmax(logits, dim=-1)
|
probs = F.softmax(logits, dim=-1)
|
||||||
token = torch.multinomial(probs, num_samples=1)
|
token = torch.multinomial(probs, num_samples=1)
|
||||||
|
|
||||||
# CLAMP the token
|
|
||||||
token = torch.clamp(token, max=max_token_id)
|
token = torch.clamp(token, max=max_token_id)
|
||||||
|
|
||||||
dream.append(token.item())
|
dream.append(token.item())
|
||||||
seed = torch.cat([seed, token], dim=1)
|
seed = torch.cat([seed, token], dim=1)
|
||||||
|
|
||||||
@ -36,15 +32,14 @@ def daydream():
|
|||||||
if score > 0.5:
|
if score > 0.5:
|
||||||
save_dream(sentence, score)
|
save_dream(sentence, score)
|
||||||
record_to_journal(sentence)
|
record_to_journal(sentence)
|
||||||
train_on_message(sentence)
|
await train_on_message(sentence)
|
||||||
|
|
||||||
if len(recent_dreams) > 10:
|
if len(recent_dreams) > 10:
|
||||||
recent_dreams.pop(0)
|
recent_dreams.pop(0)
|
||||||
|
|
||||||
|
|
||||||
def replay_dreams():
|
async def replay_dreams():
|
||||||
expand_model_if_needed()
|
await expand_model_if_needed()
|
||||||
|
|
||||||
dreams = load_dreams()
|
dreams = load_dreams()
|
||||||
context = load_context()
|
context = load_context()
|
||||||
|
|
||||||
@ -54,11 +49,10 @@ def replay_dreams():
|
|||||||
selected_dreams = random.sample(dreams, min(len(dreams), 5))
|
selected_dreams = random.sample(dreams, min(len(dreams), 5))
|
||||||
selected_contexts = random.sample(context, min(len(context), 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]
|
all_sources = [d["sentence"] for d in selected_dreams] + [c["text"] for c in selected_contexts]
|
||||||
random.shuffle(all_sources)
|
random.shuffle(all_sources)
|
||||||
|
|
||||||
mixed_sentence = " ".join(random.sample(all_sources, min(len(all_sources), 3)))
|
mixed_sentence = " ".join(random.sample(all_sources, min(len(all_sources), 3)))
|
||||||
|
|
||||||
if mixed_sentence:
|
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():
|
def load_dreams():
|
||||||
if not os.path.exists(DREAM_LOG_PATH):
|
if not os.path.exists(DREAM_LOG_PATH):
|
||||||
return []
|
return []
|
||||||
|
try:
|
||||||
with open(DREAM_LOG_PATH, "r", encoding="utf-8") as f:
|
with open(DREAM_LOG_PATH, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("[Dreams] Failed to parse dreams.json.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def save_dream(sentence: str, score: float):
|
def save_dream(sentence: str, score: float):
|
||||||
|
@ -1,43 +1,37 @@
|
|||||||
import torch
|
import torch
|
||||||
import threading
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from model.tokenizer import Tokenizer
|
from model.tokenizer import Tokenizer
|
||||||
from model.brain_state import save_model, DEVICE, model, optimizer
|
from model.brain_state import save_model, DEVICE, model, optimizer
|
||||||
|
|
||||||
tokenizer = Tokenizer()
|
tokenizer = Tokenizer()
|
||||||
expand_lock = threading.Lock()
|
expand_lock = asyncio.Lock()
|
||||||
_last_expansion_time = 0
|
_last_expansion_time = 0
|
||||||
|
|
||||||
|
|
||||||
def expand_model_if_needed():
|
async def expand_model_if_needed():
|
||||||
global _last_expansion_time
|
global _last_expansion_time
|
||||||
|
|
||||||
with expand_lock:
|
async with expand_lock:
|
||||||
# Check if expansion is actually needed
|
|
||||||
needed_vocab_size = tokenizer.next_id
|
needed_vocab_size = tokenizer.next_id
|
||||||
current_vocab_size = model.head.out_features
|
current_vocab_size = model.head.out_features
|
||||||
|
|
||||||
if needed_vocab_size <= current_vocab_size:
|
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_head_weight = model.head.weight.data
|
||||||
old_out_features = old_head_weight.size(0)
|
old_out_features = old_head_weight.size(0)
|
||||||
in_features = model.head.in_features
|
in_features = model.head.in_features
|
||||||
|
|
||||||
new_head = torch.nn.Linear(in_features, needed_vocab_size, bias=False)
|
new_head = torch.nn.Linear(in_features, needed_vocab_size, bias=False).to(DEVICE)
|
||||||
new_head = new_head.to(DEVICE)
|
|
||||||
|
|
||||||
# Copy old weights into the new head
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
new_head.weight[:old_out_features] = old_head_weight
|
new_head.weight[:old_out_features] = old_head_weight
|
||||||
|
|
||||||
model.head = new_head
|
model.head = new_head
|
||||||
|
torch.optim.lr_scheduler.StepLR(optimizer, step_size=1000, gamma=0.95)
|
||||||
# Rebuild optimizer and scheduler
|
|
||||||
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1000, gamma=0.95)
|
|
||||||
|
|
||||||
_last_expansion_time = time.time()
|
_last_expansion_time = time.time()
|
||||||
save_model()
|
save_model()
|
||||||
|
@ -26,8 +26,11 @@ def read_journal_entries():
|
|||||||
if not os.path.exists(JOURNAL_PATH):
|
if not os.path.exists(JOURNAL_PATH):
|
||||||
return []
|
return []
|
||||||
with open(JOURNAL_PATH, "r", encoding="utf-8") as f:
|
with open(JOURNAL_PATH, "r", encoding="utf-8") as f:
|
||||||
lines = f.readlines()
|
try:
|
||||||
return [line.split("|", 1)[-1].strip() for line in lines if "|" in line]
|
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):
|
def sample_journal_entries(n=5):
|
||||||
|
@ -1,30 +1,27 @@
|
|||||||
import torch
|
import torch
|
||||||
from model.brain import model, tokenizer, DEVICE
|
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.dynamic_expand import expand_model_if_needed
|
||||||
|
from model.trainer import train_on_message
|
||||||
|
|
||||||
|
|
||||||
def simulate_conversation():
|
async def simulate_conversation():
|
||||||
expand_model_if_needed()
|
await expand_model_if_needed()
|
||||||
|
|
||||||
model.eval()
|
model.eval()
|
||||||
|
|
||||||
max_token_id = model.head.out_features - 1
|
max_token_id = model.head.out_features - 1
|
||||||
if max_token_id < 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 = torch.randint(0, max_token_id + 1, (1, 5), device=DEVICE)
|
||||||
seed = seed[:, -128:] # Clamp sequence length
|
seed = seed[:, -128:]
|
||||||
|
|
||||||
output = model(seed)
|
output = model(seed)
|
||||||
|
|
||||||
preds = torch.argmax(output, dim=-1).squeeze().tolist()
|
preds = torch.argmax(output, dim=-1).squeeze().tolist()
|
||||||
if isinstance(preds, int):
|
if isinstance(preds, int):
|
||||||
preds = [preds]
|
preds = [preds]
|
||||||
|
|
||||||
# 🛡 Clamp predictions too
|
|
||||||
preds = [min(max(p, 0), max_token_id) for p in preds]
|
preds = [min(max(p, 0), max_token_id) for p in preds]
|
||||||
|
|
||||||
text = tokenizer.detokenize(preds)
|
text = tokenizer.detokenize(preds)
|
||||||
|
|
||||||
if text and len(text.split()) >= 3:
|
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():
|
async def memory_reweaver_loop():
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(600) # every 10 minutes
|
await asyncio.sleep(600) # every 10 minutes
|
||||||
expand_model_if_needed()
|
await expand_model_if_needed()
|
||||||
|
|
||||||
context = load_context()
|
context = load_context()
|
||||||
if not context:
|
if not context:
|
||||||
@ -18,4 +18,4 @@ async def memory_reweaver_loop():
|
|||||||
combined_text = " ".join([s["text"] for s in selected])
|
combined_text = " ".join([s["text"] for s in selected])
|
||||||
|
|
||||||
if combined_text:
|
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 torch
|
||||||
import time
|
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.brain_state import model, tokenizer, DEVICE, loss_fn, optimizer, scheduler
|
||||||
from model.brainmap import add_to_brainmap
|
from model.brainmap import add_to_brainmap
|
||||||
from model.journal import record_to_journal
|
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")
|
f.write(f"{time.time()},{round(value, 4)}\n")
|
||||||
|
|
||||||
|
|
||||||
def train_on_message(text: str, source: str = "user"):
|
async def train_on_message(text: str, source: str = "user"):
|
||||||
expand_model_if_needed()
|
await expand_model_if_needed()
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - _last_expansion_time < 5:
|
if now - _last_expansion_time < 5:
|
||||||
print("[Trainer] Skipping to stabilize after expansion.")
|
print("[Trainer] Skipping to stabilize after expansion.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not expand_lock.acquire(timeout=0.5):
|
|
||||||
print("[Trainer] Skipped training due to active expansion.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
model.train()
|
model.train()
|
||||||
context_texts = get_recent_context(10)
|
context_texts = get_recent_context(10)
|
||||||
|
|
||||||
# Augment the input with recent context
|
|
||||||
augmented_text = "<start> " + " ".join(context_texts + [text]) + " <end>"
|
augmented_text = "<start> " + " ".join(context_texts + [text]) + " <end>"
|
||||||
|
|
||||||
tokens = tokenizer.tokenize(augmented_text)
|
tokens = tokenizer.tokenize(augmented_text)
|
||||||
|
|
||||||
if len(tokens) < 2:
|
if len(tokens) < 2:
|
||||||
print("[Trainer] Message too short after cleaning.")
|
print("[Trainer] Message too short after cleaning.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clamp any token IDs beyond the model's output size
|
|
||||||
max_token_id = model.head.out_features - 1
|
max_token_id = model.head.out_features - 1
|
||||||
if tokenizer.next_id > model.head.out_features:
|
tokens = [max(0, min(t, max_token_id)) for t in tokens][:128]
|
||||||
expand_model_if_needed()
|
|
||||||
tokens = [t if t <= max_token_id else max_token_id for t in tokens]
|
for t in tokens:
|
||||||
tokens = tokens[:128] # Hard clamp input length
|
if t > max_token_id or t < 0:
|
||||||
|
print(f"[Trainer] Invalid token ID {t} (max={max_token_id})")
|
||||||
|
return
|
||||||
|
|
||||||
if len(tokens) < 2:
|
if len(tokens) < 2:
|
||||||
print("[Trainer] Message too short after clamping.")
|
print("[Trainer] Message too short after clamping.")
|
||||||
@ -70,11 +63,9 @@ def train_on_message(text: str, source: str = "user"):
|
|||||||
optimizer.step()
|
optimizer.step()
|
||||||
scheduler.step()
|
scheduler.step()
|
||||||
|
|
||||||
# Update brainmap and context
|
|
||||||
add_to_brainmap(augmented_text.split())
|
add_to_brainmap(augmented_text.split())
|
||||||
add_to_context(text, source=source)
|
add_to_context(text, source=source)
|
||||||
|
|
||||||
# Log training success to journal
|
|
||||||
record_to_journal({
|
record_to_journal({
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"source": source,
|
"source": source,
|
||||||
@ -82,6 +73,3 @@ def train_on_message(text: str, source: str = "user"):
|
|||||||
"loss": round(loss.item(), 4),
|
"loss": round(loss.item(), 4),
|
||||||
"vocab_size": len(tokenizer.vocab)
|
"vocab_size": len(tokenizer.vocab)
|
||||||
})
|
})
|
||||||
|
|
||||||
finally:
|
|
||||||
expand_lock.release()
|
|
||||||
|
@ -76,7 +76,7 @@ async def read_books_forever():
|
|||||||
paragraph += " " + line
|
paragraph += " " + line
|
||||||
|
|
||||||
if line[-1] in END_PUNCTUATION and len(paragraph) > PARAGRAPH_MIN_LENGTH:
|
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 = ""
|
paragraph = ""
|
||||||
await asyncio.sleep(READ_DELAY)
|
await asyncio.sleep(READ_DELAY)
|
||||||
set_next_action(READ_DELAY, "Reading")
|
set_next_action(READ_DELAY, "Reading")
|
||||||
@ -87,7 +87,7 @@ async def read_books_forever():
|
|||||||
|
|
||||||
if paragraph.strip():
|
if paragraph.strip():
|
||||||
if len(paragraph) > PARAGRAPH_MIN_LENGTH:
|
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)
|
await asyncio.sleep(READ_DELAY)
|
||||||
set_next_action(READ_DELAY, "Reading")
|
set_next_action(READ_DELAY, "Reading")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user