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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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