all of this was vibecoded. i just asked claude to fix all my bugs and he actually overhauled shit woaw. he only changed the name and position of a couple functions so if i want to write more things it should be fine. i love claude i love antrhopic i love vibecoding @claude make me a million dollars in a day make no mistake

This commit is contained in:
david committed 2026-09-08 00:36:22 -03:00
1 parent 71616cfc9b
commit 33b2688a2d
6 files changed
+441 -149

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
files files
env env
config/system_prompt.txt config/system_prompt.txt
__pycache__
+124 -119
View File
@@ -8,8 +8,8 @@ import random
import traceback import traceback
import discord import discord
import websockets import websockets
from datetime import datetime
from pathlib import Path from pathlib import Path
from utils import nots, now_str, tail_lines
def ignore_connection_closed_ok(loop, context): #this doesn't fucking work def ignore_connection_closed_ok(loop, context): #this doesn't fucking work
exc = context.get("exception") exc = context.get("exception")
@@ -19,9 +19,11 @@ def ignore_connection_closed_ok(loop, context): #this doesn't fucking work
def load_system_prompt() -> str: def load_system_prompt() -> str:
return Path("config/system_prompt.txt").read_text(encoding="utf-8") return (CONFIG_DIR / "system_prompt.txt").read_text(encoding="utf-8")
FILES_DIR = Path("files") # resolve files relative to this script, not whatever dir it happens to get launched from
SCRIPT_DIR = Path(__file__).resolve().parent
FILES_DIR = SCRIPT_DIR / "files"
LOG_DIR = FILES_DIR / "log" LOG_DIR = FILES_DIR / "log"
MEDIA_DIR = FILES_DIR / "media" MEDIA_DIR = FILES_DIR / "media"
CONFIG_DIR = FILES_DIR / "config" CONFIG_DIR = FILES_DIR / "config"
@@ -45,6 +47,10 @@ _config_lines = [l.strip() for l in (CONFIG_DIR / "tokens.txt").read_text(encodi
TOKEN = _config_lines[0] TOKEN = _config_lines[0]
server = int(_config_lines[1]) server = int(_config_lines[1])
_conn_lines = [l.strip() for l in (CONFIG_DIR / "connection.txt").read_text(encoding="utf-8").splitlines() if l.strip() and not l.strip().startswith("#")]
MAIN_URI = _conn_lines[0] # ws:// or wss:// address of main.py
MAIN_TOKEN = _conn_lines[1] # shared secret main.py checks on every payload
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
intents.members = True intents.members = True
@@ -63,9 +69,6 @@ interaction_count = 0
QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins) QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins)
def timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log_error(text: str): def log_error(text: str):
with ERROR_FILE.open("a", encoding="utf-8") as f: with ERROR_FILE.open("a", encoding="utf-8") as f:
f.write(text + "\n") f.write(text + "\n")
@@ -74,27 +77,19 @@ def log_message(text: str):
with MEMORY_FILE.open("a", encoding="utf-8") as f: with MEMORY_FILE.open("a", encoding="utf-8") as f:
f.write(text + "\n") f.write(text + "\n")
def nots(line: str) -> str:
if line.startswith("[") and "]" in line:
return line.split("]", 1)[1].lstrip()
return line
def load_shortterm() -> list[str]: def load_shortterm() -> list[str]:
try: try:
with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f: return tail_lines(MEMORY_FILE, 15) # reads bottom 15 lines without loading the whole file
lines = f.readlines()[-15:] # reads bottom 15 lines
return [line.strip() for line in lines if line.strip()]
except Exception as e: except Exception as e:
log_error(f"[{timestamp()}] error reading shortterm: {e}") log_error(f"[{now_str()}] error reading shortterm: {e}")
return [] return []
def load_longterm() -> list[str]: def load_longterm() -> list[str]:
try: try:
with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f: lines = [nots(l) for l in tail_lines(MEMORY_FILE, 515)]
lines = [nots(line.strip()) for line in f if line.strip()] return lines[:-15] if len(lines) > 15 else []
return lines[-515:-15] if len(lines) > 15 else []
except Exception as e: except Exception as e:
log_error(f"[{timestamp()}] error reading longterm: {e}") log_error(f"[{now_str()}] error reading longterm: {e}")
return [] return []
def build_payload(user_input: str, rebuild: bool = False) -> str: def build_payload(user_input: str, rebuild: bool = False) -> str:
@@ -108,22 +103,28 @@ def build_payload(user_input: str, rebuild: bool = False) -> str:
"shortterm": load_shortterm(), "shortterm": load_shortterm(),
"longterm": load_longterm(), "longterm": load_longterm(),
"rebuild_vectors": should_rebuild, "rebuild_vectors": should_rebuild,
"auth_token": MAIN_TOKEN,
}) })
_main_ws = None # kept open across messages instead of reconnecting every time
async def call_main(payload: str) -> tuple[bool, str]: async def call_main(payload: str) -> tuple[bool, str]:
uri = "ws://0.0.0.0:8765" global _main_ws
try: try:
async with websockets.connect(uri, open_timeout=None, ping_timeout=None) as websocket: if _main_ws is None or _main_ws.closed:
await websocket.send(payload) _main_ws = await websockets.connect(MAIN_URI, open_timeout=None, ping_timeout=None)
raw = await websocket.recv() await _main_ws.send(payload)
data = json.loads(raw) raw = await _main_ws.recv()
if "error" in data: data = json.loads(raw)
return False, data["error"] if "error" in data:
return True, data["ok"] return False, data["error"]
return True, data["ok"]
except websockets.exceptions.ConnectionClosedOK: except websockets.exceptions.ConnectionClosedOK:
return False, f"[{timestamp()}] websocket closed cleanly (no reply)" _main_ws = None
return False, f"[{now_str()}] websocket closed cleanly (no reply)"
except Exception as e: except Exception as e:
return False, f"[{timestamp()}] websocket error: {e}" _main_ws = None
return False, f"[{now_str()}] websocket error: {e}"
async def handle_error(error_msg: str, notify_channel=None, notify_user=None): async def handle_error(error_msg: str, notify_channel=None, notify_user=None):
print(error_msg) print(error_msg)
@@ -163,6 +164,22 @@ def steal_media(text: str):
with MEDIA_FILE.open("a", encoding="utf-8") as f: with MEDIA_FILE.open("a", encoding="utf-8") as f:
f.write(text + "\n") f.write(text + "\n")
def steal_attachments(message):
#steal images and gifs and stuff (always do this regardless of what else fires)
media_urls = [
a.url for a in message.attachments
if a.content_type and a.content_type.startswith(("image/", "video/"))
]
for embed in message.embeds:
if embed.image and embed.image.url:
media_urls.append(embed.image.url)
if embed.video and embed.video.url:
media_urls.append(embed.video.url)
for url in media_urls:
steal_media(f"[{now_str()}] saved media: {url}")
def resolve_mentions(content: str, guild) -> str: # thank you claude def resolve_mentions(content: str, guild) -> str: # thank you claude
if not guild: if not guild:
return content return content
@@ -205,17 +222,17 @@ async def random_dm():
dm = await victim.create_dm() dm = await victim.create_dm()
await dm.send(result) await dm.send(result)
dm_sent = True dm_sent = True
log_message(f"[{timestamp()}]Faggot-chan sent a random dm to {victim}: {result}") log_message(f"[{now_str()}]Faggot-chan sent a random dm to {victim}: {result}")
print(f"[{timestamp()}] random dm to {victim}: {result}") print(f"[{now_str()}] random dm to {victim}: {result}")
except Exception as e: except Exception as e:
await handle_error(f"[{timestamp()}] failed to send dm to {victim}: {e}", notify_user=victim) await handle_error(f"[{now_str()}] failed to send dm to {victim}: {e}", notify_user=victim)
async def random_message(): async def random_message():
await client.wait_until_ready() await client.wait_until_ready()
while not client.is_closed(): while not client.is_closed():
await asyncio.sleep(600) #every 10 minutes await asyncio.sleep(600) #every 10 minutes
if time.time() - last_server_message < QUIET_COOLDOWN: #only send if server has been quiet for 10 mins if time.time() - last_server_message < QUIET_COOLDOWN: #only send if server has been quiet for 10 mins
continue continue
if random.random() < 0.0001: #0.001% if random.random() < 0.0001: #0.01%
for guild in client.guilds: for guild in client.guilds:
channel = discord.utils.get(guild.text_channels, name="general") channel = discord.utils.get(guild.text_channels, name="general")
if channel: if channel:
@@ -229,8 +246,8 @@ async def random_message():
await handle_error(result, notify_channel=channel) await handle_error(result, notify_channel=channel)
continue continue
await channel.send(result) await channel.send(result)
log_message(f"[{timestamp()}] random message: {result}") log_message(f"[{now_str()}] random message: {result}")
print(f"[{timestamp()}] random message: {result}") print(f"[{now_str()}] random message: {result}")
@client.event @client.event
async def on_ready(): async def on_ready():
@@ -240,77 +257,64 @@ async def on_ready():
@client.event @client.event
async def on_random(message): async def on_random(message) -> bool:
# single weighted pick between sticker/emoji/media, used as a substitute for an actual
RNG = random.randint(1,3) # llm reply — only called once on_message already decided this message gets a response.
# returns True if it fired (caller should skip the llm call), False if it should proceed normally.
if message.author == client.user:
return
if not is_member_in_server(message.author):
return
if str(message.author.id) in block_id:
return
if not message.guild or message.guild.id != server:
return
if message.channel.name not in ["general", "announcements"]:
return
mentioned = client.user.mentioned_in(message)
#steal images and gifs and stuff (always do this regardless of what else fires)
media_urls = [
a.url for a in message.attachments
if a.content_type and a.content_type.startswith(("image/", "video/"))
]
for embed in message.embeds:
if embed.image and embed.image.url:
media_urls.append(embed.image.url)
if embed.video and embed.video.url:
media_urls.append(embed.video.url)
for url in media_urls:
steal_media(f"[{timestamp()}] saved media: {url}")
#stickers - early exit, skip ai generation
stickers = list(message.guild.stickers) stickers = list(message.guild.stickers)
if stickers and not mentioned and random.random() < 0.15: #15% chance of sending a sticker
chosen = random.choice(stickers)
try:
await message.channel.send(stickers=[chosen])
log_message(f"[{timestamp()}] Faggot-chan sent sticker: {chosen.name}")
print(f"[{timestamp()}] Faggot-chan sent sticker: {chosen.name}")
except websockets.exceptions.ConnectionClosedOK:
pass
except Exception as e:
log_error(f"[{timestamp()}] error: {e}")
await message.reply("someone tell <@1000000000000001> there is a problem with my ai")
print(f"Sticker send failed: {e}")
return
emojis = [str(e) for e in message.guild.emojis] emojis = [str(e) for e in message.guild.emojis]
if emojis and not mentioned and random.random() < 0.2: #20% of reacting - early exit
try:
await message.add_reaction(random.choice(emojis))
except websockets.exceptions.ConnectionClosedOK:
pass
except Exception as e:
log_error(f"[{timestamp()}] error: {e}")
await message.reply("someone tell <@1000000000000001> there is a problem with my ai")
print(f"Emoji reaction failed: {e}")
return
if random.random() < 0.1 and saved_media_urls: #10% of sending a stolen image - early exit, uses live set options = []
chosen = random.choice(list(saved_media_urls)) if stickers:
await message.channel.send(chosen) options.append(("sticker", 0.10))
log_message(f"[{timestamp()}] Faggot-chan sent media: {chosen}") if emojis:
print(f"[{timestamp()}] Faggot-chan sent media: {chosen}") options.append(("emoji", 0.13))
return if saved_media_urls:
options.append(("media", 0.07)) # ~30% combined chance of firing when all three are available
if not options:
return False
roll = random.random()
cumulative = 0.0
for kind, weight in options:
cumulative += weight
if roll >= cumulative:
continue
if kind == "sticker":
chosen = random.choice(stickers)
try:
await message.channel.send(stickers=[chosen])
log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
except websockets.exceptions.ConnectionClosedOK:
pass
except Exception as e:
log_error(f"[{now_str()}] error: {e}")
await message.reply("someone tell <@1000000000000001> there is a problem with my ai")
print(f"Sticker send failed: {e}")
return True
if kind == "emoji":
try:
await message.add_reaction(random.choice(emojis))
except websockets.exceptions.ConnectionClosedOK:
pass
except Exception as e:
log_error(f"[{now_str()}] error: {e}")
await message.reply("someone tell <@1000000000000001> there is a problem with my ai")
print(f"Emoji reaction failed: {e}")
return True
if kind == "media":
chosen = random.choice(list(saved_media_urls))
await message.channel.send(chosen)
log_message(f"[{now_str()}] Faggot-chan sent media: {chosen}")
print(f"[{now_str()}] Faggot-chan sent media: {chosen}")
return True
return False
@client.event @client.event
@@ -332,6 +336,9 @@ async def on_message(message):
if message.guild and message.channel.name not in ["general", "announcements"]: if message.guild and message.channel.name not in ["general", "announcements"]:
return return
if message.guild:
steal_attachments(message)
content = resolve_mentions(message.content, message.guild) content = resolve_mentions(message.content, message.guild)
# always detect replies # always detect replies
@@ -345,21 +352,19 @@ async def on_message(message):
if replied_message.author == client.user: if replied_message.author == client.user:
replied_to_bot = True replied_to_bot = True
replied_content = resolve_mentions(replied_message.content, message.guild) replied_content = resolve_mentions(replied_message.content, message.guild)
log_message(f'[{timestamp()}] {message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"') log_message(f'[{now_str()}] {message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"')
else: else:
log_message(f'[{timestamp()}] {message.author} said: {content}') log_message(f'[{now_str()}] {message.author} said: {content}')
except Exception as e: except Exception as e:
log_error(f"[{timestamp()}] failed to resolve reply: {e}") log_error(f"[{now_str()}] failed to resolve reply: {e}")
log_message(f'[{timestamp()}] {message.author} said: {content}') log_message(f'[{now_str()}] {message.author} said: {content}')
else: else:
log_message(f'[{timestamp()}] {message.author} said: {content}') log_message(f'[{now_str()}] {message.author} said: {content}')
interaction_count += 1 interaction_count += 1
rebuild = (interaction_count % 150 == 0) rebuild = (interaction_count % 150 == 0)
if message.guild is None: #dms now should work if message.guild is None: #dms now should work
if str(message.author.id) in block_id:
return
dm_sent = False dm_sent = False
if replied_message: if replied_message:
user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"' user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"'
@@ -375,11 +380,10 @@ async def on_message(message):
return return
try: try:
await message.reply(result) await message.reply(result)
print(f"[{timestamp()}] DM from {message.author}: {content} Bot replied in DM: {result}") print(f"[{now_str()}] DM from {message.author}: {content} Bot replied in DM: {result}")
log_message(f"[{timestamp()}] DM from {message.author}: {content}") log_message(f"[{now_str()}] Faggot-chan said in DM: {result}")
log_message(f"[{timestamp()}] Faggot-chan replied in DM: {result}")
except Exception as e: except Exception as e:
await handle_error(f"[{timestamp()}] failed to send DM reply: {e}", notify_user=message.author) await handle_error(f"[{now_str()}] failed to send DM reply: {e}", notify_user=message.author)
return return
mentioned = client.user.mentioned_in(message) mentioned = client.user.mentioned_in(message)
@@ -397,6 +401,9 @@ async def on_message(message):
if not mentioned and random.random() > 0.03: #3% chance of replying if not mentiond if not mentioned and random.random() > 0.03: #3% chance of replying if not mentiond
return return
if await on_random(message): # ~30% chance she just sends a sticker/emoji/media instead
return
if replied_message: if replied_message:
user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"' user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"'
else: else:
@@ -413,16 +420,14 @@ async def on_message(message):
try: try:
await message.reply(result) await message.reply(result)
print(f"[{timestamp()}] {message.author} said: {content}\n[{timestamp()}] Faggot-chan said: {result}") print(f"[{now_str()}] {message.author} said: {content}\n[{now_str()}] Faggot-chan said: {result}")
log_message(f"[{timestamp()}] {message.author} said: {content}\n[{timestamp()}] Faggot-chan said: {result}") log_message(f"[{now_str()}] Faggot-chan said: {result}")
if str(message.author.id) == moralfag_id: if str(message.author.id) == moralfag_id:
moralfag_counter += 1 moralfag_counter += 1
if moralfag_counter >= faglimit: if moralfag_counter >= faglimit:
moralfag_skip = True moralfag_skip = True
moralfag_counter = 0 moralfag_counter = 0
except Exception as e: except Exception as e:
await handle_error(f"[{timestamp()}] failed to send reply: {e}", notify_channel=message.channel) await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
await on_random(message)
client.run(TOKEN) client.run(TOKEN)
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
#all of this is vibecoded but i read it and it's okay
"""
run this in a colab cell to stand up main.py + a public wss:// tunnel to it.
session is ephemeral — restart the notebook, get a new url, re-paste it
into your bot.py-side config/connection.txt.
"""
import os
import re
import secrets
import subprocess
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_DIR = SCRIPT_DIR / "config"
CHATMODEL = "hf.co/Stetsoncody/Llama-3.2-3B-Instruct-abliterated-GGUF:Q4_K_M"
EMBEDMODEL = "hf.co/CompendiumLabs/bge-base-en-v1.5-gguf"
def run(cmd, **kwargs):
print(f"$ {cmd}")
return subprocess.run(cmd, shell=True, check=True, **kwargs)
def install_ollama():
print("\n-- installing ollama --")
run("apt install zstd | sh") #hi david here, colab doesn't come with zstd and since this script is meant for colab only it's okay to put apt
run("curl -fsSL https://ollama.com/install.sh | sh")
subprocess.Popen("ollama serve", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(5) # give the server a sec to come up before we start pulling models
def pull_models():
print("\n-- pulling models (this'll take a while) --")
run(f"ollama pull {CHATMODEL}")
run(f"ollama pull {EMBEDMODEL}")
def install_python_deps():
print("\n-- installing python deps --")
run("pip install -q ollama websockets")
def ensure_auth_token() -> str:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
token_file = CONFIG_DIR / "auth_token.txt"
if token_file.exists():
token = token_file.read_text(encoding="utf-8").strip()
print(f"\nusing existing auth token from {token_file}")
else:
token = secrets.token_urlsafe(32)
token_file.write_text(f"{token}\n", encoding="utf-8")
print(f"\ngenerated new auth token, wrote {token_file}")
return token
def ensure_params_and_prompt():
# main.py needs both of these to exist — if this is a fresh colab instance
# and you haven't uploaded them yet, stop here and add them first.
params_file = CONFIG_DIR / "params.json"
prompt_file = CONFIG_DIR / "system_prompt.txt"
missing = [f.name for f in (params_file, prompt_file) if not f.exists()]
if missing:
raise SystemExit(
f"missing {', '.join(missing)} in {CONFIG_DIR} — upload them before running this"
)
def start_main():
print("\n-- starting main.py --")
return subprocess.Popen(
f"python3 {SCRIPT_DIR / 'main.py'}",
shell=True,
)
def start_tunnel() -> str:
print("\n-- installing cloudflared --")
run("curl -fsSL -o cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb")
run("dpkg -i cloudflared.deb")
print("\n-- opening tunnel to localhost:8765 --")
proc = subprocess.Popen(
"cloudflared tunnel --url http://localhost:8765",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
url = None
deadline = time.time() + 30
for line in proc.stdout:
print(line, end="")
match = re.search(r"https://[a-zA-Z0-9\-]+\.trycloudflare\.com", line)
if match:
url = match.group(0)
break
if time.time() > deadline:
break
if not url:
raise SystemExit("couldn't find the tunnel url in cloudflared's output, check the log above")
wss_url = url.replace("https://", "wss://")
return wss_url
def main():
install_ollama()
pull_models()
install_python_deps()
ensure_params_and_prompt()
token = ensure_auth_token()
start_main()
time.sleep(3) # let main.py bind its port before the tunnel tries to reach it
wss_url = start_tunnel()
print("\n" + "=" * 60) #wtf is this shit claude
print("paste these into setup_wizard.py on the bot.py side:")
print(f" address: {wss_url}")
print(f" token: {token}")
print("=" * 60)
if __name__ == "__main__":
main()
+43 -30
View File
@@ -6,19 +6,22 @@ import ollama
import websockets import websockets
from pathlib import Path from pathlib import Path
from websockets.server import serve from websockets.server import serve
from datetime import datetime from utils import nots, now_str
chatmodel = "hf.co/Stetsoncody/Llama-3.2-3B-Instruct-abliterated-GGUF:Q4_K_M" chatmodel = "hf.co/Stetsoncody/Llama-3.2-3B-Instruct-abliterated-GGUF:Q4_K_M"
embedmodel = 'hf.co/CompendiumLabs/bge-base-en-v1.5-gguf' embedmodel = 'hf.co/CompendiumLabs/bge-base-en-v1.5-gguf'
# resolve config relative to this script, not whatever dir it happens to get launched from
SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_DIR = SCRIPT_DIR / "config"
def times() -> str: try:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S") AUTH_TOKEN = (CONFIG_DIR / "auth_token.txt").read_text(encoding="utf-8").strip()
except FileNotFoundError:
raise SystemExit("no config/auth_token.txt found — run setup_wizard.py first")
def nots(line: str) -> str: # only one generation at a time, we've got one cpu running one model here
if line.startswith("[") and "]" in line: chat_semaphore = asyncio.Semaphore(1)
return line.split("]", 1)[1].lstrip()
return line
# THANK YOU GUY FROM HUGGING FACE THAT MADE ALL THIS SO THAT I DON'T HAVE TO THINK I LOVE YOU # THANK YOU GUY FROM HUGGING FACE THAT MADE ALL THIS SO THAT I DON'T HAVE TO THINK I LOVE YOU
@@ -29,7 +32,7 @@ VECTOR_DB = []
def add_chunk_to_database(chunk): def add_chunk_to_database(chunk):
embedding = ollama.embed(model=embedmodel, input=chunk)['embeddings'][0] embedding = ollama.embed(model=embedmodel, input=chunk)['embeddings'][0]
VECTOR_DB.append((chunk, embedding)) return embedding
def cosine_similarity(a, b): def cosine_similarity(a, b):
dot_product = sum([x * y for x, y in zip(a, b)]) dot_product = sum([x * y for x, y in zip(a, b)])
@@ -47,35 +50,43 @@ def retrieve(query, top_n=5):
return similarities[:top_n] return similarities[:top_n]
def rebuild_vector_db(longterm_lines: list[str]): def rebuild_vector_db(longterm_lines: list[str]):
VECTOR_DB.clear() global VECTOR_DB
existing = dict(VECTOR_DB) # chunk -> embedding, so we don't re-embed stuff we already have
new_db = []
added = 0
for i, chunk in enumerate(longterm_lines): for i, chunk in enumerate(longterm_lines):
add_chunk_to_database(chunk) embedding = existing.get(chunk)
print(f"[{times()}] Added chunk {i+1}/{len(longterm_lines)} to the database") if embedding is None:
embedding = add_chunk_to_database(chunk)
interaction_count = 0 added += 1
print(f"[{now_str()}] embedded new chunk {added} ({i+1}/{len(longterm_lines)})")
new_db.append((chunk, embedding))
VECTOR_DB = new_db
async def ollamachat(websocket): async def ollamachat(websocket):
global interaction_count
try: try:
# bot sends one json payload with everything main needs # bot sends one json payload with everything main needs
raw = await websocket.recv() raw = await websocket.recv()
payload = json.loads(raw) payload = json.loads(raw)
if payload.get("auth_token") != AUTH_TOKEN:
await websocket.send(json.dumps({"error": "bad auth token"}))
return
system_prompt = payload["system_prompt"] system_prompt = payload["system_prompt"]
user_input = payload["user_input"] user_input = payload["user_input"]
shortterm = payload.get("shortterm", []) # list of strings shortterm = payload.get("shortterm", []) # list of strings
longterm = payload.get("longterm", []) # list of strings longterm = payload.get("longterm", []) # list of strings
interaction_count += 1 # rebuild vector db if bot signals it (every 150 interactions bot sends full longterm),
# or if we come up empty but longterm isn't — e.g. we just restarted and lost VECTOR_DB
# rebuild vector db if bot signals it (every 150 interactions bot sends full longterm) if payload.get("rebuild_vectors") or (not VECTOR_DB and longterm):
if payload.get("rebuild_vectors"): await asyncio.to_thread(rebuild_vector_db, longterm)
rebuild_vector_db(longterm) print(f"[{now_str()}] Vector DB reconciled, {len(VECTOR_DB)} chunks")
print(f"[{times()}] Vector DB rebuilt with {len(longterm)} chunks")
user_query = nots(user_input) user_query = nots(user_input)
retrieved_knowledge = retrieve(user_query) if VECTOR_DB else [] retrieved_knowledge = await asyncio.to_thread(retrieve, user_query) if VECTOR_DB else []
# rag chunks go in system so they don't pollute turn history # rag chunks go in system so they don't pollute turn history
rag_block = "" rag_block = ""
@@ -151,24 +162,26 @@ async def ollamachat(websocket):
messages.insert(0, {"role": "system", "content": system_prompt + rag_block}) messages.insert(0, {"role": "system", "content": system_prompt + rag_block})
params = json.loads(Path("config/params.json").read_text()) params = json.loads((CONFIG_DIR / "params.json").read_text())
response = ollama.chat( async with chat_semaphore:
model=chatmodel, response = await asyncio.to_thread(
messages=messages, ollama.chat,
options=params, model=chatmodel,
) messages=messages,
options=params,
)
# strip <think> blocks in case the model ignores think=False, thank you claude # strip <think> blocks in case the model ignores think=False, thank you claude
bot_reply = re.sub(r'<think>.*?</think>', '', response["message"]["content"], flags=re.DOTALL).strip() bot_reply = re.sub(r'<think>.*?</think>', '', response["message"]["content"], flags=re.DOTALL).strip()
print(messages) print(messages)
print(f"[{times()}] reply: {bot_reply}\n") print(f"[{now_str()}] reply: {bot_reply}\n")
await websocket.send(json.dumps({"ok": bot_reply})) await websocket.send(json.dumps({"ok": bot_reply}))
except Exception as e: except Exception as e:
error_msg = f"[{times()}] error in ollamachat: {e}" error_msg = f"[{now_str()}] error in ollamachat: {e}"
print(error_msg) print(error_msg)
try: try:
await websocket.send(json.dumps({"error": error_msg})) await websocket.send(json.dumps({"error": error_msg}))
@@ -177,7 +190,7 @@ async def ollamachat(websocket):
async def main(): async def main():
print(f"[{times()}] hosting on ws://0.0.0.0:8765") print(f"[{now_str()}] hosting on ws://0.0.0.0:8765")
async with serve(ollamachat, "0.0.0.0", 8765, ping_interval=None, ping_timeout=None): async with serve(ollamachat, "0.0.0.0", 8765, ping_interval=None, ping_timeout=None):
await asyncio.Future() await asyncio.Future()
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
setup wizard for faggot-chan. figures out which files need writing based on
how you're deploying, and writes them. run this before bot.py / main.py.
"""
import secrets
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
def ask(prompt: str, default: str = None) -> str:
suffix = f" [{default}]" if default else ""
val = input(f"{prompt}{suffix}: ").strip()
return val or default or ""
def ask_yn(prompt: str) -> bool:
while True:
val = input(f"{prompt} (y/n): ").strip().lower()
if val in ("y", "yes"):
return True
if val in ("n", "no"):
return False
print("just y or n please")
def write_tokens_file(config_dir: Path):
config_dir.mkdir(parents=True, exist_ok=True)
print("\n-- discord bot setup --")
token = ask("discord bot token")
server_id = ask("discord server (guild) id")
while not server_id.isdigit():
print("that's not a number")
server_id = ask("discord server (guild) id")
(config_dir / "tokens.txt").write_text(f"{token}\n{server_id}\n", encoding="utf-8")
print(f"wrote {config_dir / 'tokens.txt'}")
def write_connection_file(config_dir: Path, default_uri: str = "ws://0.0.0.0:8765"):
config_dir.mkdir(parents=True, exist_ok=True)
print("\n-- connection to main.py --")
uri = ask("main.py address (ws:// or wss://)", default_uri)
print("paste the auth token main.py is using (from colab_setup.py output),")
token = ask("or leave blank to generate a new one")
if not token:
token = secrets.token_urlsafe(32)
print(f"generated token: {token}")
(config_dir / "connection.txt").write_text(f"{uri}\n{token}\n", encoding="utf-8")
print(f"wrote {config_dir / 'connection.txt'}")
return token
def write_auth_token_file(config_dir: Path, token: str = None):
config_dir.mkdir(parents=True, exist_ok=True)
if not token:
print("\n-- websocket auth for main.py --")
print("this is the secret bot.py has to send. leave blank to generate one.")
token = ask("auth token")
if not token:
token = secrets.token_urlsafe(32)
print(f"generated token: {token}")
(config_dir / "auth_token.txt").write_text(f"{token}\n", encoding="utf-8")
print(f"wrote {config_dir / 'auth_token.txt'}")
print(f"\ngive this token (and this machine's public wss:// url) to whoever's setting up bot.py:\n {token}")
def main():
print("faggot-chan setup wizard\n")
colab = ask_yn("is this for a colab-hosted main.py")
if not colab:
# everything lives together, plain local setup
bot_config = SCRIPT_DIR / "files" / "config"
main_config = SCRIPT_DIR / "config"
write_tokens_file(bot_config)
token = write_connection_file(bot_config, default_uri="ws://0.0.0.0:8765")
write_auth_token_file(main_config, token=token)
print("\nall set. run main.py, then bot.py.")
return
where = ask("running this on the 'vps' (bot.py host) or 'colab' box (main.py host)", "vps")
if where.lower().startswith("colab"):
# this box just needs the auth token main.py will check — connection details
# (discord token, server id) live on whichever box runs bot.py
main_config = SCRIPT_DIR / "config"
write_auth_token_file(main_config)
print("\nnow run colab_setup.py to bring main.py + the tunnel up.")
return
# vps / bot.py host: skip discord-client setup entirely (handled separately),
# this preset only wires up the connection to the colab-hosted main.py
bot_config = SCRIPT_DIR / "files" / "config"
if not (bot_config / "tokens.txt").exists():
print("\nheads up: no files/config/tokens.txt found here — the vps preset skips")
print("discord token setup, so make sure that file gets added separately.")
write_connection_file(bot_config, default_uri="wss://your-colab-tunnel-url-here")
print("\nall set. run bot.py once main.py is up on the colab side.")
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
from pathlib import Path
from datetime import datetime
def now_str() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def nots(line: str) -> str:
if line.startswith("[") and "]" in line:
return line.split("]", 1)[1].lstrip()
return line
def tail_lines(path: Path, n: int) -> list[str]:
# grabs the last n lines without reading the whole file into memory first,
# since memory.txt only ever grows and reading it fully every message doesn't scale
if n <= 0:
return []
avg_line_bytes = 200
with path.open("rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
block_size = n * avg_line_bytes
data = b""
lines = []
while True:
seek_pos = max(file_size - block_size, 0)
f.seek(seek_pos)
data = f.read(file_size - seek_pos)
lines = data.splitlines()
if len(lines) > n or seek_pos == 0:
break
block_size *= 2
return [l.decode("utf-8", errors="ignore").strip() for l in lines[-n:] if l.strip()]