mirror of
https://git.squeakyrat.moe/faggot-chan/
synced 2026-11-04 18:35:45 +01:00
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:
1 parent
71616cfc9b
commit
33b2688a2d
6 files changed
+441
-149
No files matched your search
@@ -1,3 +1,4 @@
|
||||
files
|
||||
env
|
||||
config/system_prompt.txt
|
||||
__pycache__
|
||||
@@ -8,8 +8,8 @@ import random
|
||||
import traceback
|
||||
import discord
|
||||
import websockets
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from utils import nots, now_str, tail_lines
|
||||
|
||||
def ignore_connection_closed_ok(loop, context): #this doesn't fucking work
|
||||
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:
|
||||
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"
|
||||
MEDIA_DIR = FILES_DIR / "media"
|
||||
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]
|
||||
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.message_content = 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)
|
||||
|
||||
def timestamp() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def log_error(text: str):
|
||||
with ERROR_FILE.open("a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
@@ -74,27 +77,19 @@ def log_message(text: str):
|
||||
with MEMORY_FILE.open("a", encoding="utf-8") as f:
|
||||
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]:
|
||||
try:
|
||||
with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = f.readlines()[-15:] # reads bottom 15 lines
|
||||
return [line.strip() for line in lines if line.strip()]
|
||||
return tail_lines(MEMORY_FILE, 15) # reads bottom 15 lines without loading the whole file
|
||||
except Exception as e:
|
||||
log_error(f"[{timestamp()}] error reading shortterm: {e}")
|
||||
log_error(f"[{now_str()}] error reading shortterm: {e}")
|
||||
return []
|
||||
|
||||
def load_longterm() -> list[str]:
|
||||
try:
|
||||
with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = [nots(line.strip()) for line in f if line.strip()]
|
||||
return lines[-515:-15] if len(lines) > 15 else []
|
||||
lines = [nots(l) for l in tail_lines(MEMORY_FILE, 515)]
|
||||
return lines[:-15] if len(lines) > 15 else []
|
||||
except Exception as e:
|
||||
log_error(f"[{timestamp()}] error reading longterm: {e}")
|
||||
log_error(f"[{now_str()}] error reading longterm: {e}")
|
||||
return []
|
||||
|
||||
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(),
|
||||
"longterm": load_longterm(),
|
||||
"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]:
|
||||
uri = "ws://0.0.0.0:8765"
|
||||
global _main_ws
|
||||
try:
|
||||
async with websockets.connect(uri, open_timeout=None, ping_timeout=None) as websocket:
|
||||
await websocket.send(payload)
|
||||
raw = await websocket.recv()
|
||||
data = json.loads(raw)
|
||||
if "error" in data:
|
||||
return False, data["error"]
|
||||
return True, data["ok"]
|
||||
if _main_ws is None or _main_ws.closed:
|
||||
_main_ws = await websockets.connect(MAIN_URI, open_timeout=None, ping_timeout=None)
|
||||
await _main_ws.send(payload)
|
||||
raw = await _main_ws.recv()
|
||||
data = json.loads(raw)
|
||||
if "error" in data:
|
||||
return False, data["error"]
|
||||
return True, data["ok"]
|
||||
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:
|
||||
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):
|
||||
print(error_msg)
|
||||
@@ -163,6 +164,22 @@ def steal_media(text: str):
|
||||
with MEDIA_FILE.open("a", encoding="utf-8") as f:
|
||||
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
|
||||
if not guild:
|
||||
return content
|
||||
@@ -205,17 +222,17 @@ async def random_dm():
|
||||
dm = await victim.create_dm()
|
||||
await dm.send(result)
|
||||
dm_sent = True
|
||||
log_message(f"[{timestamp()}]Faggot-chan sent a random dm to {victim}: {result}")
|
||||
print(f"[{timestamp()}] random dm to {victim}: {result}")
|
||||
log_message(f"[{now_str()}]Faggot-chan sent a random dm to {victim}: {result}")
|
||||
print(f"[{now_str()}] random dm to {victim}: {result}")
|
||||
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():
|
||||
await client.wait_until_ready()
|
||||
while not client.is_closed():
|
||||
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
|
||||
continue
|
||||
if random.random() < 0.0001: #0.001%
|
||||
if random.random() < 0.0001: #0.01%
|
||||
for guild in client.guilds:
|
||||
channel = discord.utils.get(guild.text_channels, name="general")
|
||||
if channel:
|
||||
@@ -229,8 +246,8 @@ async def random_message():
|
||||
await handle_error(result, notify_channel=channel)
|
||||
continue
|
||||
await channel.send(result)
|
||||
log_message(f"[{timestamp()}] random message: {result}")
|
||||
print(f"[{timestamp()}] random message: {result}")
|
||||
log_message(f"[{now_str()}] random message: {result}")
|
||||
print(f"[{now_str()}] random message: {result}")
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
@@ -240,77 +257,64 @@ async def on_ready():
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_random(message):
|
||||
|
||||
RNG = random.randint(1,3)
|
||||
|
||||
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
|
||||
async def on_random(message) -> bool:
|
||||
# single weighted pick between sticker/emoji/media, used as a substitute for an actual
|
||||
# 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.
|
||||
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]
|
||||
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
|
||||
chosen = random.choice(list(saved_media_urls))
|
||||
await message.channel.send(chosen)
|
||||
log_message(f"[{timestamp()}] Faggot-chan sent media: {chosen}")
|
||||
print(f"[{timestamp()}] Faggot-chan sent media: {chosen}")
|
||||
return
|
||||
options = []
|
||||
if stickers:
|
||||
options.append(("sticker", 0.10))
|
||||
if emojis:
|
||||
options.append(("emoji", 0.13))
|
||||
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
|
||||
@@ -332,6 +336,9 @@ async def on_message(message):
|
||||
if message.guild and message.channel.name not in ["general", "announcements"]:
|
||||
return
|
||||
|
||||
if message.guild:
|
||||
steal_attachments(message)
|
||||
|
||||
content = resolve_mentions(message.content, message.guild)
|
||||
|
||||
# always detect replies
|
||||
@@ -345,21 +352,19 @@ async def on_message(message):
|
||||
if replied_message.author == client.user:
|
||||
replied_to_bot = True
|
||||
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:
|
||||
log_message(f'[{timestamp()}] {message.author} said: {content}')
|
||||
log_message(f'[{now_str()}] {message.author} said: {content}')
|
||||
except Exception as e:
|
||||
log_error(f"[{timestamp()}] failed to resolve reply: {e}")
|
||||
log_message(f'[{timestamp()}] {message.author} said: {content}')
|
||||
log_error(f"[{now_str()}] failed to resolve reply: {e}")
|
||||
log_message(f'[{now_str()}] {message.author} said: {content}')
|
||||
else:
|
||||
log_message(f'[{timestamp()}] {message.author} said: {content}')
|
||||
log_message(f'[{now_str()}] {message.author} said: {content}')
|
||||
|
||||
interaction_count += 1
|
||||
rebuild = (interaction_count % 150 == 0)
|
||||
|
||||
if message.guild is None: #dms now should work
|
||||
if str(message.author.id) in block_id:
|
||||
return
|
||||
dm_sent = False
|
||||
if replied_message:
|
||||
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
|
||||
try:
|
||||
await message.reply(result)
|
||||
print(f"[{timestamp()}] DM from {message.author}: {content} Bot replied in DM: {result}")
|
||||
log_message(f"[{timestamp()}] DM from {message.author}: {content}")
|
||||
log_message(f"[{timestamp()}] Faggot-chan replied in DM: {result}")
|
||||
print(f"[{now_str()}] DM from {message.author}: {content} Bot replied in DM: {result}")
|
||||
log_message(f"[{now_str()}] Faggot-chan said in DM: {result}")
|
||||
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
|
||||
|
||||
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
|
||||
return
|
||||
|
||||
if await on_random(message): # ~30% chance she just sends a sticker/emoji/media instead
|
||||
return
|
||||
|
||||
if replied_message:
|
||||
user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"'
|
||||
else:
|
||||
@@ -413,16 +420,14 @@ async def on_message(message):
|
||||
|
||||
try:
|
||||
await message.reply(result)
|
||||
print(f"[{timestamp()}] {message.author} said: {content}\n[{timestamp()}] Faggot-chan said: {result}")
|
||||
log_message(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"[{now_str()}] Faggot-chan said: {result}")
|
||||
if str(message.author.id) == moralfag_id:
|
||||
moralfag_counter += 1
|
||||
if moralfag_counter >= faglimit:
|
||||
moralfag_skip = True
|
||||
moralfag_counter = 0
|
||||
except Exception as e:
|
||||
await handle_error(f"[{timestamp()}] failed to send reply: {e}", notify_channel=message.channel)
|
||||
|
||||
await on_random(message)
|
||||
await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
|
||||
|
||||
client.run(TOKEN)
|
||||
+131
@@ -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()
|
||||
@@ -6,19 +6,22 @@ import ollama
|
||||
import websockets
|
||||
from pathlib import Path
|
||||
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"
|
||||
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:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
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:
|
||||
if line.startswith("[") and "]" in line:
|
||||
return line.split("]", 1)[1].lstrip()
|
||||
return line
|
||||
# only one generation at a time, we've got one cpu running one model here
|
||||
chat_semaphore = asyncio.Semaphore(1)
|
||||
|
||||
|
||||
# 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):
|
||||
embedding = ollama.embed(model=embedmodel, input=chunk)['embeddings'][0]
|
||||
VECTOR_DB.append((chunk, embedding))
|
||||
return embedding
|
||||
|
||||
def cosine_similarity(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]
|
||||
|
||||
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):
|
||||
add_chunk_to_database(chunk)
|
||||
print(f"[{times()}] Added chunk {i+1}/{len(longterm_lines)} to the database")
|
||||
|
||||
interaction_count = 0
|
||||
embedding = existing.get(chunk)
|
||||
if embedding is None:
|
||||
embedding = add_chunk_to_database(chunk)
|
||||
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):
|
||||
global interaction_count
|
||||
try:
|
||||
# bot sends one json payload with everything main needs
|
||||
raw = await websocket.recv()
|
||||
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"]
|
||||
user_input = payload["user_input"]
|
||||
shortterm = payload.get("shortterm", []) # 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)
|
||||
if payload.get("rebuild_vectors"):
|
||||
rebuild_vector_db(longterm)
|
||||
print(f"[{times()}] Vector DB rebuilt with {len(longterm)} chunks")
|
||||
# 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
|
||||
if payload.get("rebuild_vectors") or (not VECTOR_DB and longterm):
|
||||
await asyncio.to_thread(rebuild_vector_db, longterm)
|
||||
print(f"[{now_str()}] Vector DB reconciled, {len(VECTOR_DB)} chunks")
|
||||
|
||||
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_block = ""
|
||||
@@ -151,24 +162,26 @@ async def ollamachat(websocket):
|
||||
|
||||
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(
|
||||
model=chatmodel,
|
||||
messages=messages,
|
||||
options=params,
|
||||
)
|
||||
async with chat_semaphore:
|
||||
response = await asyncio.to_thread(
|
||||
ollama.chat,
|
||||
model=chatmodel,
|
||||
messages=messages,
|
||||
options=params,
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
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}))
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"[{times()}] error in ollamachat: {e}"
|
||||
error_msg = f"[{now_str()}] error in ollamachat: {e}"
|
||||
print(error_msg)
|
||||
try:
|
||||
await websocket.send(json.dumps({"error": error_msg}))
|
||||
@@ -177,7 +190,7 @@ async def ollamachat(websocket):
|
||||
|
||||
|
||||
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):
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
+104
@@ -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()
|
||||
@@ -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()]
|
||||
Reference in new issue
Block a user