1 Commits

Author SHA1 Message Date
4ddcd2b374 env vars testing 2026-01-04 05:36:35 -06:00
15 changed files with 162 additions and 451 deletions

View File

@@ -22,7 +22,8 @@ jobs:
if [ "$(docker compose ps -q)" ]; then
docker compose down
fi
docker compose up -d --build
docker build -t git.crate.zip/crate/garfbot:latest .
docker compose up -d
else
docker restart garfbot
fi

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ config.py
__pycache__/
garfpy/__pycache__/
*.venv*
.idea
*.json
*.old
*.log*

View File

@@ -18,7 +18,6 @@ RUN apk update && \
tcl-dev
RUN pip3 install --no-cache-dir \
config \
discord \
openai \
aiohttp \

View File

@@ -1,6 +1,6 @@
MIT No Attribution
Copyright 2026 Aaron Crate
Copyright 2025 Aaron Crate
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software

View File

@@ -2,19 +2,14 @@ services:
garfbot:
image: git.crate.zip/crate/garfbot:latest
container_name: garfbot
build:
context: .
dockerfile: Dockerfile
environment:
- TXT_MODEL=${TXT_MODEL}
- IMG_MODEL=${IMG_MODEL}
- GARFBOT_TOKEN=${GARFBOT_TOKEN}
- OPENAI_TOKEN=${OPENAI_TOKEN}
- WEATHER_TOKEN=${WEATHER_TOKEN}
- KROGER_ID=${KROGER_ID}
- KROGER_SECRET=${KROGER_SECRET}
restart: always
volumes:
- /home/crate/garfbot:/usr/src/app
jonbot:
image: git.crate.zip/crate/jonbot:latest
container_name: jonbot
build:
context: ./jonbot
dockerfile: Dockerfile
restart: always
volumes:
- /home/crate/garfbot/jonbot:/usr/src/app

53
etc/jonbot.py Normal file
View File

@@ -0,0 +1,53 @@
import config
import openai
import discord
import asyncio
import os
openai.api_key = config.OPENAI_TOKEN
jonkey = config.JONBOT_TOKEN
model = config.TXT_MODEL
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Logged in as {client.user.name} running {model}.", flush=True)
@client.event
async def on_message(message):
if message.author == client.user:
return
if message.content.lower().startswith("hey jon") or isinstance(message.channel, discord.DMChannel):
question = message.content[7:] if message.content.lower().startswith("hey jon") else message.content
try:
response = openai.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": "Pretend you are friendly Jon Arbuckle."},
{"role": "user", "content": f"{question}"}
],
max_tokens=400
)
answer = response['choices'][0]['message']['content']
answer = answer.replace("AI language model", "American citizen")
answer = answer.replace("language model AI", "citizen of the United States")
await message.channel.send(answer)
except Exception as e:
e = str(e)
await message.channel.send(f"`JonBot Error: {e}`")
async def jonbot_connect():
while True:
try:
await client.start(jonkey)
except Exception as e:
e = str(e)
logger.error(f"Jonbot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(jonbot_connect())

View File

@@ -1,8 +1,7 @@
import config
import os
import asyncio
import discord
from discord.ext import commands
from urllib.parse import urlparse, parse_qs
from garfpy import (
help,
@@ -17,10 +16,9 @@ from garfpy import (
)
# gapikey = config.GIF_TOKEN
garfkey = config.GARFBOT_TOKEN
txtmodel = config.TXT_MODEL
imgmodel = config.IMG_MODEL
garfkey = os.getenv("GARFBOT_TOKEN")
txtmodel = os.getenv("TXT_MODEL")
imgmodel = os.getenv("IMG_MODEL")
intents = discord.Intents.default()
intents.members = True
@@ -41,36 +39,6 @@ kroger = Kroger()
weather = WeatherAPI()
URL_PATTERNS = [
r'https?://(?:www\.)?youtube\.com/watch\?[^\s]*',
r'https?://youtu\.be/[^\s]*',
r'https?://(?:open\.)?spotify\.com/[^\s]*',
]
def clean_url(url):
try:
parsed = urlparse(url)
if 'youtube.com' in parsed.hostname:
params = parse_qs(parsed.query)
video_id = params.get('v', [None])[0]
if not video_id:
return None
# timestamp = params.get('t', [None])[0]
# if timestamp:
# return f"https://www.youtube.com/watch?v={video_id}&t={timestamp}"
return f"https://www.youtube.com/watch?v={video_id}"
if 'youtu.be' in parsed.hostname:
return f"https://youtu.be{parsed.path}"
if 'spotify.com' in parsed.hostname:
return f"https://open.spotify.com{parsed.path}"
except Exception:
return None
@garfbot.event
async def on_ready():
try:
@@ -116,30 +84,30 @@ async def garfbot_qr(ctx, *, text):
f"QR Code Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Text: {text}"
)
if len(text) > 1000:
await ctx.reply("❌ Text too long! Maximum 1000 characters.")
await ctx.send("❌ Text too long! Maximum 1000 characters.")
else:
try:
qr_code = await generate_qr(text)
sendfile = discord.File(fp=qr_code, filename="qrcode.png")
await ctx.reply(file=sendfile)
await ctx.send(file=sendfile)
except Exception as e:
logger.error(e)
await ctx.reply(e)
await ctx.send(e)
@garfbot.command(name="wiki")
async def garfbot_wiki(ctx, *, query):
summary = await garfield.wikisum(query)
await ctx.reply(summary)
await ctx.send(summary)
@garfbot.command(name="shop")
async def garfbot_shop(ctx, *, query):
try:
response = kroger.garfshop(query)
await ctx.reply(response)
await ctx.send(response)
except Exception as e:
await ctx.reply(f"`GarfBot Error: {str(e)}`")
await ctx.send(f"`GarfBot Error: {str(e)}`")
@garfbot.command(name="weather")
@@ -149,15 +117,11 @@ async def garfbot_weather(ctx, *, location):
@garfbot.command(name="chat")
async def garfchat(ctx, *, prompt):
if "is this true" in prompt.lower():
messages = [msg async for msg in ctx.channel.history(limit=2)]
prompt = messages[1].content
prompt = f"Is this true: {prompt}"
answer = await garfield.generate_chat(prompt)
logger.info(
f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.reply(answer)
await ctx.send(answer)
@garfbot.command(name="pic")
@@ -165,7 +129,7 @@ async def garfpic(ctx, *, prompt):
logger.info(
f"Image Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.reply(f"`Please wait... image generation queued: {prompt}`")
await ctx.send(f"`Please wait... image generation queued: {prompt}`")
await garfield.garfpic(ctx, prompt)
@@ -183,19 +147,6 @@ async def on_message(message):
content = message.content.strip()
lower = content.lower()
# # Remove tracking stuff from youtube and spotify links
# cleaned_urls = []
# for pattern in URL_PATTERNS:
# for match in re.finditer(pattern, message.content):
# cleaned = clean_url(match.group(0))
# if cleaned and cleaned != match.group(0):
# cleaned_urls.append(cleaned)
# if cleaned_urls:
# links = '\n'.join(cleaned_urls)
# await message.reply(f"🔗 Cleaned link{'s' if len(cleaned_urls) > 1 else ''}:\n{links}")
# Chats & pics
if lower.startswith("hey garfield") or isinstance(
message.channel, discord.DMChannel
@@ -229,7 +180,7 @@ async def on_message(message):
async def garfbot_connect():
while True:
try:
await garfbot.start(garfkey)
await garfbot.start(garfkey) # type: ignore
except Exception as e:
e = str(e)
logger.error(f"Garfbot couldn't connect! {e}")

View File

@@ -1,11 +1,12 @@
# garfpy/__init__.py
from .log import logger as logger
from .help import help as help
from .kroger import Kroger as Kroger
from .garfai import GarfAI as GarfAI
from .respond import GarfbotRespond as GarfbotRespond
from .aod import aod_message as aod_message
from .qr import generate_qr as generate_qr
from .iputils import IPUtils as IPUtils
from .weather import WeatherAPI as WeatherAPI
from .log import logger
from .help import help
from .kroger import Kroger
from .kroger import Kroger
from .garfai import GarfAI
from .respond import GarfbotRespond
from .aod import aod_message
from .qr import generate_qr
from .iputils import IPUtils
from .weather import WeatherAPI

View File

@@ -121,17 +121,3 @@ async def aod_message(garfbot, message):
for field, values in zip(table_fields, table_columns):
stats_embed.add_field(name=field, value="\n".join(values), inline=True)
await message.channel.send(embed=stats_embed)
# # Boy You Said It
# words = re.findall(r"[a-zA-Z']+", message.content.lower())
# stops = {"a", "an", "the", "and", "or", "but", "is", "it", "in", "on", "at", "to", "of"}
# words = [w for w in words if w not in stops]
# if words:
# firsts = [w[0] for w in words]
# commons = max(set(firsts), key=firsts.count)
# count = firsts.count(commons)
# if count >= 3 or (len(words) >= 2 and count / len(words) >= 0.75):
# await message.channel.send("Boy, you said it!")

View File

@@ -1,8 +1,6 @@
import io
import time
import uuid
import os
import openai
import config
import aiohttp
import asyncio
import discord
@@ -11,212 +9,90 @@ from openai import AsyncOpenAI
from garfpy import logger
INVOKEAI_BASE = config.INVOKEAI_URL
_MODEL_KEY = "0eb50094-5c9b-431b-ba01-87e145edb849"
_VAE_KEY = "dde3627c-8a45-4088-93d1-66c44acbb337"
_ENCODER_KEY = "7ba22542-4687-4946-a52e-c92f925f4b75"
_MODEL_REF = {"key": _MODEL_KEY, "hash": "blake3:c3ee838d71d99497db01fae6f304eafd9e734e935f3b783e968d50febb56be2c", "name": "FLUX.2 Klein 4B (GGUF Q4)", "base": "flux2", "type": "main"}
_VAE_REF = {"key": _VAE_KEY, "hash": "blake3:531855de70db993d0f6181f82cde27d15411d58b7ffa3b2fdce2b9434c0173c2", "name": "FLUX.2 VAE", "base": "flux2", "type": "vae"}
_ENCODER_REF = {"key": _ENCODER_KEY, "hash": "blake3:af5840e6770dc99f678e69867949c8b9264835915eb82a990e940fa6e4fa6c81", "name": "FLUX.2 Klein Qwen3 4B Encoder", "base": "any", "type": "qwen3_encoder"}
_POLL_INTERVAL = 2
_POLL_ATTEMPTS = 60
_MIN_IMAGE_BYTES = 1000
def _node_id(prefix: str) -> str:
return f"{prefix}:{uuid.uuid4().hex[:10]}"
def _build_graph(prompt: str) -> dict:
seed = int(time.time() * 1000) % (2 ** 31)
p = _node_id("positive_prompt")
ml = _node_id("flux2_klein_model_loader")
te = _node_id("flux2_klein_text_encoder")
dn = _node_id("flux2_denoise")
out = _node_id("canvas_output")
nodes = {
p: {"id": p, "is_intermediate": True, "use_cache": True, "value": prompt, "type": "string"},
ml: {"id": ml, "is_intermediate": True, "use_cache": True, "type": "flux2_klein_model_loader",
"model": _MODEL_REF, "vae_model": _VAE_REF, "qwen3_encoder_model": _ENCODER_REF},
te: {"id": te, "is_intermediate": True, "use_cache": True, "type": "flux2_klein_text_encoder"},
dn: {"id": dn, "is_intermediate": True, "use_cache": True, "type": "flux2_denoise", "seed": seed},
out: {"id": out, "is_intermediate": False, "use_cache": False, "type": "flux2_vae_decode"},
}
edges = [
{"source": {"node_id": ml, "field": "qwen3_encoder"}, "destination": {"node_id": te, "field": "qwen3_encoder"}},
{"source": {"node_id": ml, "field": "max_seq_len"}, "destination": {"node_id": te, "field": "max_seq_len"}},
{"source": {"node_id": p, "field": "value"}, "destination": {"node_id": te, "field": "prompt"}},
{"source": {"node_id": ml, "field": "transformer"}, "destination": {"node_id": dn, "field": "transformer"}},
{"source": {"node_id": ml, "field": "vae"}, "destination": {"node_id": dn, "field": "vae"}},
{"source": {"node_id": te, "field": "conditioning"}, "destination": {"node_id": dn, "field": "positive_text_conditioning"}},
{"source": {"node_id": ml, "field": "vae"}, "destination": {"node_id": out, "field": "vae"}},
{"source": {"node_id": dn, "field": "latents"}, "destination": {"node_id": out, "field": "latents"}},
]
return {"nodes": nodes, "edges": edges}
async def _poll_batch(session: aiohttp.ClientSession, base: str, batch_id: str) -> bool:
"""Poll batch status until completed, failed, or timed out. Returns True on success."""
for _ in range(_POLL_ATTEMPTS):
await asyncio.sleep(_POLL_INTERVAL)
try:
async with session.get(f"{base}/api/v1/queue/default/b/{batch_id}/status") as resp:
if not resp.ok:
continue
s = await resp.json(content_type=None)
total = s.get("total", 0)
completed = s.get("completed", 0)
failed = s.get("failed", 0)
if total > 0 and failed >= total:
logger.error(f"Batch {batch_id} failed")
return False
if total > 0 and completed >= total:
return True
except Exception as e:
logger.error(f"InvokeAI poll error: {e}")
return False
async def _get_image_name(session: aiohttp.ClientSession, base: str, batch_id: str) -> str | None:
try:
async with session.get(f"{base}/api/v1/queue/default/i/{batch_id}") as resp:
if resp.ok:
data = await resp.json(content_type=None)
for node_result in data.get("session", {}).get("results", {}).values():
if node_result.get("type") == "image_output":
return node_result["image"]["image_name"]
except Exception as e:
logger.error(f"Item fetch error: {e}")
try:
async with session.get(
f"{base}/api/v1/images/",
params={"limit": 1, "order_by": "created_at", "direction": "DESC"},
) as resp:
if resp.ok:
items = (await resp.json(content_type=None)).get("items", [])
if items:
return items[0]["image_name"]
except Exception as e:
logger.error(f"Image list fallback error: {e}")
return None
async def _fetch_image_bytes(session: aiohttp.ClientSession, base: str, name: str) -> bytes | None:
"""Try the full image endpoint, then fall back to thumbnail."""
urls = [
f"{base}/api/v1/images/i/{name}/full",
f"{base}/api/v1/images/i/{name}/thumbnail",
]
for url in urls:
try:
async with session.get(url) as resp:
ct = resp.headers.get("Content-Type", "")
data = await resp.read()
logger.info(f"Image fetch {url}: status={resp.status} content-type={ct} size={len(data)}")
if "html" not in ct and len(data) >= _MIN_IMAGE_BYTES:
return data
except Exception as e:
logger.error(f"InvokeAI image fetch error ({url}): {e}")
return None
class GarfAI:
def __init__(self):
self.baseurl = config.BASE_URL
self.sysprompt = config.SYSTEM_PROMPT
self.txtmodel = config.TXT_MODEL
self.imgmodel = config.IMG_MODEL
self._oai = AsyncOpenAI(
api_key=config.OPENAI_TOKEN,
base_url=config.BASE_URL,
)
self.openaikey = os.getenv('OPENAI_TOKEN')
self.txtmodel = os.getenv('TXT_MODEL')
self.imgmodel = os.getenv('IMG_MODEL')
self.image_request_queue = asyncio.Queue()
async def garfpic(self, ctx, prompt):
await self.image_request_queue.put({"ctx": ctx, "prompt": prompt})
async def generate_image(self, session: aiohttp.ClientSession, prompt: str) -> bytes | str:
base = INVOKEAI_BASE
async def generate_image(self, prompt):
client = AsyncOpenAI(api_key=self.openaikey)
try:
async with session.post(
f"{base}/api/v1/queue/default/enqueue_batch",
json={"batch": {"graph": _build_graph(prompt), "runs": 1}},
) as resp:
if not resp.ok:
text = await resp.text()
logger.error(f"InvokeAI enqueue failed {resp.status}: {text}")
return "`GarfBot Error: InvokeAI rejected the request`"
data = await resp.json(content_type=None)
batch_id = data["batch"]["batch_id"]
response = await client.images.generate(
model=self.imgmodel, prompt=prompt, n=1, size="1024x1024"
)
except openai.BadRequestError as e:
return f"`GarfBot Error: ({e.status_code}) - Your request was rejected as a result of our safety system.`"
except openai.InternalServerError as e:
logger.error(e)
return f"`GarfBot Error: ({e.status_code}) - Monday`"
except Exception as e:
logger.error(f"InvokeAI enqueue error: {e}")
return "`GarfBot Error: Couldn't reach InvokeAI`"
logger.error(e)
return "`GarfBot Error: Lasagna`"
data = getattr(response, "data", None)
if not data:
logger.error("No data in response")
return "`GarfBot Error: No images generated`"
logger.info(f"InvokeAI batch queued: {batch_id}")
first_image = data[0] if len(data) > 0 else None
if not first_image:
logger.error("No image in response data")
return "`GarfBot Error: No images generated`"
if not await _poll_batch(session, base, batch_id):
return "`GarfBot Error: InvokeAI generation failed or timed out`"
image_url = getattr(first_image, "url", None)
if not image_url:
logger.error("No URL in image response")
return "`GarfBot Error: No image URL returned`"
image_name = await _get_image_name(session, base, batch_id)
if not image_name:
return "`GarfBot Error: Could not resolve image name`"
logger.info(f"Got image: {image_name}")
data = await _fetch_image_bytes(session, base, image_name)
if data:
return data
logger.error("All image download attempts failed")
return "`GarfBot Error: Odie`"
return image_url
async def process_image_requests(self):
async with aiohttp.ClientSession(headers={"Accept": "application/json"}) as session:
async with aiohttp.ClientSession() as session:
while True:
request = await self.image_request_queue.get()
ctx = request["ctx"]
ctx = request["ctx"]
prompt = request["prompt"]
result = await self.generate_image(session, prompt)
if isinstance(result, bytes):
logger.info("Sending image...")
image = io.BytesIO(result)
timestamp = ctx.message.created_at.strftime("%Y%m%d%H%M%S")
filename = f"{timestamp}_generated_image.png"
try:
await ctx.reply(file=discord.File(fp=image, filename=filename))
except Exception as e:
logger.error(e)
image_url = await self.generate_image(prompt)
if image_url and "GarfBot Error" not in image_url:
logger.info("Downloading & sending image...")
async with session.get(image_url) as resp:
if resp.status == 200:
image_data = await resp.read()
image = io.BytesIO(image_data)
image.seek(0)
timestamp = ctx.message.created_at.strftime("%Y%m%d%H%M%S")
filename = f"{timestamp}_generated_image.png"
sendfile = discord.File(fp=image, filename=filename)
try:
await ctx.send(file=sendfile)
except Exception as e:
logger.error(e)
else:
await ctx.send("`GarfBot Error: Odie`")
else:
await ctx.reply(result)
await ctx.send(image_url)
self.image_request_queue.task_done()
await asyncio.sleep(2)
async def generate_chat(self, question: str) -> str:
async def generate_chat(self, question):
try:
response = await self._oai.chat.completions.create(
model=self.txtmodel,
client = AsyncOpenAI(api_key=self.openaikey)
response = await client.chat.completions.create(
model=self.txtmodel, # type: ignore
messages=[
{"role": "system", "content": self.sysprompt},
{"role": "user", "content": question},
{
"role": "system",
"content": "Pretend you are sarcastic Garfield.",
},
{"role": "user", "content": f"{question}"},
],
max_tokens=400,
temperature=1.2,
)
answer = response.choices[0].message.content
answer = str(response.choices[0].message.content)
return answer.replace("an AI language model", "a cartoon animal")
except openai.BadRequestError as e:
logger.error(e)
@@ -228,15 +104,12 @@ class GarfAI:
logger.error(e)
return "`GarfBot Error: Lasagna`"
async def wikisum(self, query: str) -> str:
async def wikisum(self, query):
try:
summary = wikipedia.summary(query)
return await self.generate_chat(f"Please summarize in your own words: {summary}")
except wikipedia.exceptions.DisambiguationError as e:
options = ", ".join(e.options[:3])
return f"`GarfBot Error: Ambiguous query — did you mean: {options}?`"
except wikipedia.exceptions.PageError:
return "`GarfBot Error: No Wikipedia page found for that query`"
garfsum = await self.generate_chat(
f"Please summarize in your own words: {summary}"
)
return garfsum
except Exception as e:
logger.error(e)
return f"`GarfBot Error: {e}`"
return e

View File

@@ -6,9 +6,9 @@ async def help(message):
embed.add_field(
name="hey garfield `prompt`", value="*Responds with text.*", inline=True
)
# embed.add_field(
# name="garfpic `prompt`", value="*Responds with an image.*", inline=True
# )
embed.add_field(
name="garfpic `prompt`", value="*Responds with an image.*", inline=True
)
embed.add_field(
name="garfping `target`",
value="*Responds with iputils-ping result from target.*",

View File

@@ -1,4 +1,4 @@
import config
import os
import requests
from base64 import b64encode
from garfpy import logger
@@ -6,8 +6,8 @@ from garfpy import logger
class Kroger:
def __init__(self):
self.client_id = config.CLIENT_ID
self.client_secret = config.CLIENT_SECRET
self.client_id = os.getenv('CLIENT_ID')
self.client_secret = os.getenv('CLIENT_SECRET')
self.auth = b64encode(
f"{self.client_id}:{self.client_secret}".encode()
).decode()

View File

@@ -1,5 +1,5 @@
import os
import re
import config
import discord
import aiohttp
from garfpy import logger
@@ -7,7 +7,7 @@ from garfpy import logger
class WeatherAPI:
def __init__(self, api_key=None):
self.api_key = api_key or config.WEATHER_TOKEN
self.api_key = api_key or os.getenv("WEATHER_TOKEN")
self.base_url = "https://api.openweathermap.org/data/2.5/weather"
def parse_location(self, location):
@@ -126,7 +126,7 @@ class WeatherAPI:
response.raise_for_status()
return await response.json()
except aiohttp.ClientError as e:
logger.error(f"Error fetching weather data for '{location}' - {e}")
logger.error(f"Error fetching weather data for '{location}'")
await ctx.send(f"`Error fetching weather data for '{location}'`")
return None

View File

@@ -1,30 +0,0 @@
FROM python:alpine
WORKDIR /usr/src/app
RUN apk update && \
apk add --no-cache \
iputils \
bind-tools \
nmap \
gcc \
musl-dev \
jpeg-dev \
zlib-dev \
freetype-dev \
lcms2-dev \
openjpeg-dev \
tiff-dev \
tk-dev \
tcl-dev
RUN pip3 install --no-cache-dir \
config \
discord \
openai \
aiohttp \
requests \
wikipedia \
pillow \
qrcode
CMD [ "python", "jonbot.py" ]

View File

@@ -1,119 +0,0 @@
import config
import openai
from openai import AsyncOpenAI
import discord
from discord.ext import commands
import asyncio
import logging
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger("garflog")
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
file_handler = TimedRotatingFileHandler(
"jonbot.log",
when="midnight",
interval=1,
backupCount=7,
delay=True,
)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
if not logger.hasHandlers():
logger.addHandler(file_handler)
logger.addHandler(console_handler)
openai.api_key = config.OPENAI_TOKEN
jonkey = config.JONBOT_TOKEN
txtmodel = config.TXT_MODEL
sysprompt = config.SYSTEM_PROMPT
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
client = commands.Bot(
command_prefix=["Jonbot ", "jonbot ", "Jon", "jon"],
case_insensitive=True,
intents=intents,
)
@client.event
async def on_ready():
print(f"Logged in as {client.user.name} running {txtmodel}.", flush=True)
@client.command(name="chat")
async def jonchat(ctx, *, prompt):
if "is this true" in prompt.lower():
messages = [msg async for msg in ctx.channel.history(limit=2)]
prompt = messages[1].content
prompt = f"Is this true: {prompt}"
answer = await generate_chat(prompt)
logger.info(
f"Chat Request - User: {ctx.author.name}, Server: {ctx.guild.name}, Prompt: {prompt}"
)
await ctx.reply(answer)
@client.event
async def on_message(message):
if message.author == client.user:
return
content = message.content.strip()
lower = content.lower()
if lower.startswith("hey jon") or isinstance(
message.channel, discord.DMChannel
):
ctx = await client.get_context(message)
await jonchat(ctx, prompt=content)
oai = AsyncOpenAI(
api_key=config.OPENAI_TOKEN,
base_url=config.BASE_URL,
)
async def generate_chat(question: str) -> str:
try:
response = await oai.chat.completions.create(
model=txtmodel,
messages=[
{"role": "system", "content": sysprompt},
{"role": "user", "content": question},
],
max_tokens=400,
temperature=1.2,
)
answer = response.choices[0].message.content
return answer
except openai.BadRequestError as e:
logger.error(e)
return f"`JonBot Error: {e}`"
except openai.APIError as e:
logger.error(e)
return "`JonBot Error: Odie`"
except Exception as e:
logger.error(e)
return "`JonBot Error: Liz`"
async def jonbot_connect():
while True:
try:
await client.start(jonkey)
except Exception as e:
e = str(e)
logger.error(f"Jonbot couldn't connect! {e}")
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(jonbot_connect())