c91ec03b83
CI / compile (pull_request) Successful in 5s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 26s
build / build (push) Successful in 12s
CI / compile (push) Successful in 5s
CI / unit (push) Successful in 22s
CI / integration (push) Successful in 26s
Two things the field report asked for. 1) PERSONAL ASSISTANTS (replacing the sunset OpenAI Assistants API) The old implementation gave three capabilities. Two are reimplemented here, the third was confirmed unused and is deliberately not replaced: * per-user persona - it already lived in system_gpt_settings.json; it was only ever being shipped to OpenAI. It is now the system prompt. * per-user conversation thread - OpenAI held this server-side. It now lives in assistant_memory.json, keyed by discord user id, trimmed to the most recent turns (CONJURER_ASSISTANT_MEMORY_TURNS) and written atomically so a torn write cannot lose someone's history. Deliberately a plain trim, not the AI summarisation used for the bar's shared memory: these are private DMs and must not end up in a public "legend". * file_search - not replaced. Confirmed not in use. The conversation goes through handle_response with request_type="NONE" and an explicit message list, which keeps it out of the bar's shared memory. The big win: create_chat_assistant hardcoded model="gpt-4o", so assistants were locked to OpenAI. They now run on whatever $gadaj_teraz selects - Claude and Ollama included. create_chat_assistant / chat_with_assistant are gone, and with them the last call to beta.threads in the startup path - so the cog cannot be killed by that API again. (add_files_to_vector_store / delete_files_from_vector_store still reference beta.assistants but are dead code - nothing calls them - so they cannot crash anything; left alone rather than widening this change.) 2) KEEPING A SELF-HOSTED MODEL WARM Loading is the slow part - the GPU is shared with other users - so we preload via Ollama's documented mechanism: /api/generate with a model, a keep_alive and NO prompt. It loads the model and generates nothing. * on switching to ollama, $gadaj_teraz fires a preload in the BACKGROUND (not awaited: loading can take minutes and the command must answer at once), so the wait lands on the operator rather than the first user; * a warm loop re-asserts keep_alive every CONJURER_OLLAMA_WARM_MINUTES. Both are hard-guarded on the ACTIVE provider being ollama. Warming a metered API would burn tokens and money for nothing, so that guard is pinned by a test asserting the preload is never called for gpt/claude, and another asserting the preload body carries no prompt (a prompt would make every warm-up generate). Tests: 82 unit + 71 integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
626 lines
29 KiB
Python
626 lines
29 KiB
Python
# ai command cogs
|
|
import asyncio
|
|
import logging
|
|
import re
|
|
import sys
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
import discord
|
|
import openai
|
|
import requests
|
|
from queue import Empty
|
|
from discord.ext import commands, tasks
|
|
from other_functions import discord_friendly_send, discord_friendly_reply
|
|
from communication_subroutine import AI_QUERY_Q
|
|
|
|
|
|
import ai_functions
|
|
from constants import (
|
|
OLLAMA_WARM_MINUTES,
|
|
DATA,
|
|
GRAPHICS_PATH,
|
|
INITIAL_TIME_WAIT,
|
|
MASTER_TIMEOUT,
|
|
MESSAGE_TABLE,
|
|
OPENAICLIENT,
|
|
SPECJALNE_ZIEMNIACZKI,
|
|
WORD_REACTIONS,
|
|
)
|
|
|
|
|
|
class Dm_Mode(Enum):
|
|
ARMIA_HAMMERA = (1,)
|
|
SPECJALNY_ZIEMNIACZEK = (2,)
|
|
SEKRETNY_SEKSRET = (3,)
|
|
ECHO_ECHO = (4,)
|
|
|
|
|
|
class Events(commands.Cog):
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.logger = logging.getLogger("discord")
|
|
self.armia = {}
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
self.armia[superfryta[0]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK
|
|
self.logger.info(self.armia)
|
|
|
|
@tasks.loop(seconds=2)
|
|
async def ai_query_worker(self):
|
|
"""Drain AI_QUERY_Q one prompt at a time and answer with the active backend.
|
|
|
|
This is the bot-side half of the AI query interface: prompts arrive over
|
|
HTTP (POST /ai_query) or in-process (submit_ai_query), get queued, and are
|
|
answered here with handle_response - so they automatically use whichever
|
|
provider $gadaj_teraz currently selects. The answer is posted to the
|
|
channel the request named.
|
|
"""
|
|
try:
|
|
item = AI_QUERY_Q.get(block=False)
|
|
except Empty:
|
|
return
|
|
prompt = item.get("prompt", "")
|
|
request_type = item.get("request_type", "NONE")
|
|
channel_id = item.get("channel_id")
|
|
username = item.get("username", "conjurer")
|
|
self.logger.info(
|
|
"AI query from %s (%s) -> channel %s", username, request_type, channel_id
|
|
)
|
|
global MESSAGE_TABLE # pylint: disable=global-statement
|
|
try:
|
|
if request_type == "NONE":
|
|
# Clean one-shot: no persona system prompt, no memory write.
|
|
result, _ = await ai_functions.handle_response(
|
|
"", True, True, [], username, "NONE", none_request=prompt
|
|
)
|
|
else:
|
|
result, MESSAGE_TABLE = await ai_functions.handle_response(
|
|
prompt, True, True, MESSAGE_TABLE, username, request_type
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
self.logger.exception("AI query failed: %s", exc)
|
|
result = "*Kondziu drapie się po głowie* Coś się zjebało przy pytaniu do AI."
|
|
if channel_id is None:
|
|
self.logger.warning("AI query had no channel_id - answer dropped")
|
|
return
|
|
channel = self.bot.get_channel(channel_id)
|
|
if channel is None:
|
|
self.logger.warning("AI query channel %s not found - answer dropped", channel_id)
|
|
return
|
|
await self._send_chunked(channel, result)
|
|
|
|
@ai_query_worker.before_loop
|
|
async def _before_ai_query_worker(self):
|
|
await self.bot.wait_until_ready()
|
|
|
|
async def _send_chunked(self, channel, text):
|
|
"""Send text in <=1900-char pieces (Discord caps messages at 2000)."""
|
|
text = text or ""
|
|
while text:
|
|
await discord_friendly_send(channel, text[:1900])
|
|
text = text[1900:]
|
|
|
|
async def cog_load(self):
|
|
# The AI query worker answers via handle_response, so it works on every
|
|
# backend. Start it first.
|
|
if not self.ai_query_worker.is_running():
|
|
self.ai_query_worker.start()
|
|
# Keeps a self-hosted model resident; it no-ops on any other provider.
|
|
if not self.ollama_warm_loop.is_running():
|
|
self.ollama_warm_loop.start()
|
|
# NOTE: there is no OpenAI-Assistants bootstrap any more. It called a
|
|
# sunset API (beta threads), 404'd, and failed the WHOLE extension -
|
|
# taking every AI command with it. Personal assistants now ride
|
|
# handle_response with per-user memory (ai_functions), so they work on
|
|
# Claude and Ollama too and nothing has to be created at startup.
|
|
self.logger.info("Osobiści asystenci: pamięć per-user, aktywny backend AI")
|
|
|
|
@tasks.loop(minutes=OLLAMA_WARM_MINUTES)
|
|
async def ollama_warm_loop(self):
|
|
"""Keep a self-hosted model resident so users don't pay the load wait.
|
|
|
|
Loading is the slow part on a GPU shared with other users, so we
|
|
re-assert Ollama's keep_alive well inside its window. This preloads
|
|
WITHOUT generating - no tokens, no cost.
|
|
|
|
Hard guard: it does nothing unless the ACTIVE backend is Ollama. Firing
|
|
warm-ups at a metered API would burn tokens and money for nothing.
|
|
"""
|
|
try:
|
|
if ai_functions.active_provider() != "ollama":
|
|
return
|
|
await ai_functions.warm_active_model()
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.logger.info("Rozgrzewanie Ollamy nieudane (nieszkodliwe): %s", exc)
|
|
|
|
@ollama_warm_loop.before_loop
|
|
async def before_ollama_warm_loop(self):
|
|
await self.bot.wait_until_ready()
|
|
|
|
async def cog_unload(self):
|
|
self.ai_query_worker.cancel()
|
|
self.ollama_warm_loop.cancel()
|
|
|
|
@commands.hybrid_command(
|
|
name="switch_dm_mode",
|
|
description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj",
|
|
guild=discord.Object(id=664789470779932693),
|
|
)
|
|
async def switch_dm_mode(self, ctx, dm_mode_arg: Dm_Mode):
|
|
async with ctx.channel.typing():
|
|
if isinstance(ctx.channel, discord.DMChannel):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
self.logger.info(ctx.message.author.id)
|
|
self.logger.info(superfryta)
|
|
if ctx.message.author.id == superfryta[0]:
|
|
self.armia[ctx.message.author.id] = dm_mode_arg
|
|
self.logger.info(self.armia)
|
|
await ctx.reply("Weszlo")
|
|
return
|
|
await ctx.reply(
|
|
"Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich"
|
|
)
|
|
else:
|
|
await ctx.reply("Nope. Nie wiesz jak użyć")
|
|
|
|
@commands.hybrid_command(
|
|
name="modele_ai",
|
|
description="Pokaż modele dostępne dla danego backendu (Ollamę pyta na żywo).",
|
|
)
|
|
async def modele_ai(self, ctx, nazwa_konfigu: Optional[str] = None):
|
|
"""Read-only model listing. For Ollama this queries the server, so it
|
|
shows exactly what is pulled on the box right now."""
|
|
async with ctx.channel.typing():
|
|
target = nazwa_konfigu or ai_functions.get_active_ai_config()
|
|
if target not in ai_functions.list_ai_configs():
|
|
await discord_friendly_reply(
|
|
ctx,
|
|
f"Nie znam configu '{target}'. Dostępne: "
|
|
f"{', '.join(ai_functions.list_ai_configs())}",
|
|
)
|
|
return
|
|
try:
|
|
models = await ai_functions.list_provider_models(target)
|
|
except ai_functions.AIError as exc:
|
|
await discord_friendly_reply(
|
|
ctx, f"Nie mogę pobrać modeli dla '{target}': {exc}"
|
|
)
|
|
return
|
|
if not models:
|
|
await discord_friendly_reply(ctx, f"Brak modeli dla '{target}'.")
|
|
return
|
|
await discord_friendly_reply(
|
|
ctx,
|
|
f"Modele dla **{target}**: {', '.join(models)}\n"
|
|
f"Wepniesz przez `$gadaj_teraz {target} <model>` (tylko Vykidailo).",
|
|
)
|
|
|
|
@commands.hybrid_command(
|
|
name="gadaj_teraz",
|
|
description="Pokaż/przełącz backend AI i model (bez argumentu = status). Przełączanie: Vykidailo.",
|
|
)
|
|
async def gadaj_teraz(
|
|
self, ctx, nazwa_konfigu: Optional[str] = None, model: Optional[str] = None
|
|
):
|
|
async with ctx.channel.typing():
|
|
available = ai_functions.list_ai_configs()
|
|
# No argument -> report the active backend (read-only, open to all).
|
|
if not nazwa_konfigu:
|
|
active = ai_functions.get_active_ai_config()
|
|
active_cfg = ai_functions.AI_CONFIGS.get(active, {})
|
|
await discord_friendly_reply(
|
|
ctx,
|
|
f"Teraz gadam przez **{active}** "
|
|
f"({active_cfg.get('provider')} / {active_cfg.get('latest_model')}). "
|
|
f"Dostępne: {', '.join(available)}. "
|
|
"Przełączysz przez `$gadaj_teraz <config> [model]` (tylko Vykidailo), "
|
|
"modele zobaczysz przez `$modele_ai`.",
|
|
)
|
|
return
|
|
is_admin = isinstance(ctx.author, discord.Member) and any(
|
|
role.name == "Vykidailo" for role in ctx.author.roles
|
|
)
|
|
if not is_admin:
|
|
await discord_friendly_reply(ctx, "Tylko Vykidailo może przełączać AI.")
|
|
return
|
|
if nazwa_konfigu not in available:
|
|
await discord_friendly_reply(
|
|
ctx,
|
|
f"Nie znam configu '{nazwa_konfigu}'. Dostępne: {', '.join(available)}",
|
|
)
|
|
return
|
|
try:
|
|
cfg = ai_functions.set_active_ai_config(nazwa_konfigu)
|
|
except (KeyError, RuntimeError) as exc:
|
|
await discord_friendly_reply(
|
|
ctx, f"Nie mogę przełączyć na '{nazwa_konfigu}': {exc}"
|
|
)
|
|
return
|
|
|
|
# Optional second argument pins the model. For a backend we can
|
|
# enumerate (Ollama), reject an unknown id up front with the list -
|
|
# otherwise the typo only surfaces later as a failed reply.
|
|
if model:
|
|
try:
|
|
known = await ai_functions.list_provider_models(nazwa_konfigu)
|
|
except ai_functions.AIError:
|
|
known = [] # cannot enumerate -> accept verbatim
|
|
if known and cfg.get("provider") == "ollama" and model not in known:
|
|
await discord_friendly_reply(
|
|
ctx,
|
|
f"Model '{model}' nie jest wgrany na Ollamie. "
|
|
f"Dostępne: {', '.join(known)}",
|
|
)
|
|
return
|
|
try:
|
|
cfg = ai_functions.set_active_model(model, nazwa_konfigu)
|
|
except (KeyError, ValueError) as exc:
|
|
await discord_friendly_reply(
|
|
ctx, f"Nie mogę wpiąć modelu '{model}': {exc}"
|
|
)
|
|
return
|
|
|
|
self.logger.info(
|
|
"Przełączono AI na config %s (%s / %s)",
|
|
nazwa_konfigu, cfg.get("provider"), cfg.get("latest_model"),
|
|
)
|
|
message = (
|
|
f"Teraz gadam przez **{nazwa_konfigu}** — "
|
|
f"{cfg.get('provider')} / {cfg.get('latest_model')}."
|
|
)
|
|
if cfg.get("provider") == "ollama":
|
|
# Pay the (slow, shared-GPU) load cost NOW, in the background,
|
|
# so it lands on the operator switching backends rather than on
|
|
# whoever sends the first message. Not awaited: loading can take
|
|
# minutes and the command must answer immediately.
|
|
asyncio.create_task(ai_functions.warm_active_model())
|
|
message += (
|
|
"\nRozgrzewam model w tle — pierwsza odpowiedź może chwilę potrwać."
|
|
)
|
|
# Switched without pinning a model: show what else is on offer.
|
|
if not model:
|
|
try:
|
|
others = await ai_functions.list_provider_models(nazwa_konfigu)
|
|
except ai_functions.AIError:
|
|
others = []
|
|
current = cfg.get("latest_model")
|
|
if others and current not in others:
|
|
# The configured/pinned model is not on the server: every
|
|
# reply would fail with "model not found" and nothing would
|
|
# say why. Flag it here, where the list is already in hand.
|
|
message += (
|
|
f"\n⚠ Uwaga: '{current}' nie jest wgrany na serwerze. "
|
|
f"Dostępne: {', '.join(others)} "
|
|
f"(`$gadaj_teraz {nazwa_konfigu} <model>`)."
|
|
)
|
|
elif len(others) > 1:
|
|
message += (
|
|
f"\nDostępne modele: {', '.join(others)} "
|
|
f"(`$gadaj_teraz {nazwa_konfigu} <model>`)."
|
|
)
|
|
await discord_friendly_reply(ctx, message)
|
|
|
|
@commands.hybrid_command(
|
|
name="armia_hammera",
|
|
description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj",
|
|
)
|
|
async def armia_hammera(
|
|
self, ctx, message_txt: str, recipient: Optional[discord.User]
|
|
):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
if ctx.message.author.id == superfryta[0]:
|
|
await self.armia_hammera_back(ctx, message_txt, recipient)
|
|
return
|
|
await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich")
|
|
|
|
async def armia_hammera_back(self, ctx, message_txt, recipient=None):
|
|
self.logger.info("Armia Hammera")
|
|
recipients = []
|
|
if recipient:
|
|
recipients.append(recipient.id)
|
|
else:
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
recipients.append(superfryta[0])
|
|
self.logger.info(recipients)
|
|
for item in recipients:
|
|
user = await self.bot.fetch_user(item)
|
|
channel = await user.create_dm()
|
|
self.logger.info(
|
|
"User %s -> %s: %s", ctx.message.author, user, message_txt
|
|
)
|
|
await discord_friendly_send(channel, message_txt)
|
|
await discord_friendly_reply(ctx, "Poszło")
|
|
|
|
#TODO: NOT IMPLEMENTED YET
|
|
@commands.Cog.listener(name="dodaj_do_bazy_wiedzy")
|
|
async def dodaj_do_bazy_wiedzy(self, ctx):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
if ctx.message.author.id == superfryta[0]:
|
|
#logic here
|
|
return
|
|
await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich")
|
|
|
|
#TODO: NOT IMPLEMENTED YET
|
|
@commands.Cog.listener(name="listuj_baze_wiedzy")
|
|
async def listuj_baze_wiedzy(self, ctx):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
if ctx.message.author.id == superfryta[0]:
|
|
#logic here
|
|
return
|
|
await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich")
|
|
|
|
#TODO: NOT IMPLEMENTED YET
|
|
@commands.Cog.listener(name="usun_z_bazy_wiedzy")
|
|
async def usun_z_bazy_wiedzy(self, ctx):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
if ctx.message.author.id == superfryta[0]:
|
|
#logic here
|
|
return
|
|
await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich")
|
|
|
|
#TODO: NOT IMPLEMENTED YET
|
|
@commands.Cog.listener(name="przetworz_plik_linia_po_linii")
|
|
async def przetworz_plik_linia_po_linii(self, ctx):
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
if ctx.message.author.id == superfryta[0]:
|
|
#logic here
|
|
return
|
|
await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich")
|
|
|
|
@commands.Cog.listener()
|
|
async def on_message(self, message):
|
|
"""
|
|
Handle incoming messages in a Discord server, perform various
|
|
checks and actions based on the content and context of the message, and respond accordingly.
|
|
|
|
:param message: The message object that is received when a user sends a message in a Discord server
|
|
or DM. The code is checking various conditions and performing actions based on the content of the
|
|
message and the context in which it was sent. It also includes TODOs for future improvements
|
|
:return: The function `on_message` is being returned.
|
|
"""
|
|
vykidailo = False
|
|
channel = None
|
|
if message.author == self.bot.user:
|
|
return
|
|
|
|
if isinstance(message.author, discord.Member):
|
|
for role in message.author.roles:
|
|
if role.name == "Vykidailo":
|
|
vykidailo = True
|
|
|
|
if ("Conjurer Śpij Słodko Aniołku" in message.content) and vykidailo:
|
|
sys.exit()
|
|
# kanal bez bota
|
|
if message.channel.id == 1095985579147141202:
|
|
return
|
|
# wentylacja
|
|
if message.channel.id == 1083804024173764739:
|
|
return
|
|
# legendy
|
|
if message.channel.id == 1084448332841230388:
|
|
return
|
|
# interrogation booth
|
|
if message.channel.id == 1111625221171052595:
|
|
return
|
|
if isinstance(message.channel, discord.DMChannel):
|
|
self.logger.info(message.author.id)
|
|
for superfryta in SPECJALNE_ZIEMNIACZKI.values():
|
|
self.logger.info(superfryta[0])
|
|
|
|
if message.author.id == superfryta[0]:
|
|
self.logger.info("Specjalny ziemniak")
|
|
if self.armia[message.author.id] == Dm_Mode.SPECJALNY_ZIEMNIACZEK:
|
|
# superfryta = [discord_id, assistant_name, owner, instructions, legacy_assistant_id]
|
|
await ai_functions.chat_with_personal_assistant(
|
|
message, superfryta[2], superfryta[3]
|
|
)
|
|
return
|
|
elif self.armia[message.author.id] == Dm_Mode.ECHO_ECHO:
|
|
await ai_functions.echo(message)
|
|
return
|
|
elif self.armia[message.author.id] == Dm_Mode.ARMIA_HAMMERA:
|
|
self.logger.info("Armia Hammera get context")
|
|
ctx = await self.bot.get_context(message)
|
|
self.logger.info("Armia Hammera function call")
|
|
await self.armia_hammera_back(ctx=ctx, message_txt=message.content)
|
|
return
|
|
elif self.armia[message.author.id] == Dm_Mode.SEKRETNY_SEKSRET:
|
|
pass
|
|
else:
|
|
await discord_friendly_send(message.channel,"Coś się wyebao. Wołaj Hammera")
|
|
return
|
|
channel = self.bot.get_channel(1064888712565100614)
|
|
await discord_friendly_send(channel, "Słyszałem ja żem że: " + message.content)
|
|
return
|
|
channel = message.channel
|
|
message_content_lower = message.content.lower()
|
|
|
|
tdelta = datetime.now() - MASTER_TIMEOUT
|
|
tdelta = tdelta.total_seconds()
|
|
if "opowiedz o fabryczce" in message_content_lower:
|
|
await message.reply(DATA["fabryczka"])
|
|
if "opowiedz mi o fabryczce" in message_content_lower:
|
|
await message.reply(DATA["fabryczka"])
|
|
|
|
if tdelta > INITIAL_TIME_WAIT:
|
|
for word in WORD_REACTIONS:
|
|
if re.search(r"\b" + word + r"\b", message_content_lower):
|
|
tdelta = datetime.now() - WORD_REACTIONS[word][2]
|
|
tdelta = tdelta.total_seconds()
|
|
security_clearance = WORD_REACTIONS[word][4]
|
|
reaction = WORD_REACTIONS[word][3]
|
|
if tdelta > WORD_REACTIONS[word][1]:
|
|
# TODO: to zrobic reactiony
|
|
self.logger.info("Ping z procedury reakcji")
|
|
if reaction:
|
|
emoji = self.bot.get_emoji(WORD_REACTIONS[word][0])
|
|
await message.add_reaction(emoji)
|
|
elif security_clearance and vykidailo:
|
|
await message.reply(WORD_REACTIONS[word][0])
|
|
elif not security_clearance:
|
|
await message.reply(WORD_REACTIONS[word][0])
|
|
WORD_REACTIONS[word][2] = datetime.now()
|
|
|
|
# TODO: drobne literówki, mentiony, spacja przed dwukropkiem. napraw.
|
|
kondziu_mentioned = False
|
|
for mention in message.mentions:
|
|
if mention == self.bot.user:
|
|
kondziu_mentioned = True
|
|
|
|
if kondziu_mentioned or "conjurer:" in message_content_lower:
|
|
async with channel.typing():
|
|
self.logger.info("Procedura chatu")
|
|
|
|
message_content_lower = message_content_lower.replace("conjurer: ", "")
|
|
if message.author.nick:
|
|
username = message.author.nick
|
|
else:
|
|
username = message.author.name
|
|
vykidailo = False
|
|
bartender = False
|
|
if kondziu_mentioned:
|
|
prompt = message.clean_content
|
|
else:
|
|
prompt = message.content
|
|
for role in message.author.roles:
|
|
if role.name == "Vykidailo":
|
|
vykidailo = True
|
|
if role.name == "Bartender":
|
|
bartender = True
|
|
global MESSAGE_TABLE # pylint: disable=global-statement
|
|
|
|
result, MESSAGE_TABLE = await ai_functions.handle_response(
|
|
prompt,
|
|
vykidailo,
|
|
bartender,
|
|
MESSAGE_TABLE,
|
|
username,
|
|
"GENERAL",
|
|
)
|
|
|
|
await discord_friendly_reply(message, result)
|
|
if "imaginuje sobie:" in message.content:
|
|
async with channel.typing():
|
|
self.logger.info("Poczatek procedury obrazkowej")
|
|
# Image generation is DALL-E (OpenAI); there is no Anthropic
|
|
# equivalent, so it stays on OpenAI regardless of the chat
|
|
# backend. Degrade gracefully when OpenAI isn't configured.
|
|
if OPENAICLIENT is None:
|
|
await discord_friendly_reply(
|
|
message,
|
|
"*Kondziu rozkłada łapska* — malowanie obrazków jest teraz wyłączone (brak OpenAI).",
|
|
)
|
|
return
|
|
message_content_lower = message_content_lower.replace("imaginuje sobie: ", "")
|
|
self.logger.debug("Wywolanie obrazka: %s", message_content_lower)
|
|
# Every error branch below must RETURN: otherwise control falls
|
|
# through to `if response:` with `response` unbound (the call
|
|
# raised) -> UnboundLocalError, crashing the handler right after
|
|
# the friendly message was already sent.
|
|
response = None
|
|
try:
|
|
response = await OPENAICLIENT.images.generate(
|
|
model="dall-e-3",
|
|
prompt=message.content,
|
|
size="1024x1024",
|
|
quality="standard",
|
|
n=1,
|
|
)
|
|
except openai.APITimeoutError as e:
|
|
# Handle timeout error, e.g. retry or log
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}"
|
|
)
|
|
return
|
|
except openai.APIConnectionError as e:
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}"
|
|
)
|
|
return
|
|
except openai.BadRequestError as e:
|
|
# Handle invalid request error, e.g. validate parameters or log
|
|
if message.author.nick:
|
|
username = message.author.nick
|
|
else:
|
|
username = message.author.name
|
|
resp, _ = await ai_functions.handle_response(
|
|
f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie '{message.content}'",
|
|
True,
|
|
True,
|
|
MESSAGE_TABLE,
|
|
username,
|
|
"GENERAL",
|
|
)
|
|
await discord_friendly_reply(
|
|
message, f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}"
|
|
)
|
|
return
|
|
except openai.AuthenticationError as e:
|
|
# Handle authentication error, e.g. check credentials or log
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}"
|
|
)
|
|
return
|
|
except openai.PermissionDeniedError as e:
|
|
# Handle permission error, e.g. check scope or log
|
|
# (was accidentally passing a (message, text) TUPLE as one arg)
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}"
|
|
)
|
|
return
|
|
except openai.RateLimitError as e:
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}"
|
|
)
|
|
return
|
|
except openai.APIError as e:
|
|
# Handle API error, e.g. retry or log
|
|
await discord_friendly_reply(
|
|
message, f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}"
|
|
)
|
|
return
|
|
if response:
|
|
self.logger.info(response)
|
|
image_url = response.data[0].url
|
|
image_desc = response.data[0].revised_prompt
|
|
self.logger.debug("Wynikowy obrazek pod url: %s", image_url)
|
|
response = requests.get(image_url, timeout=360)
|
|
temp_file_name = message.content + ".png"
|
|
temp_file_name = GRAPHICS_PATH + message.content + ".png"
|
|
num = 0
|
|
while (Path(temp_file_name)).exists():
|
|
temp_file_name = GRAPHICS_PATH + message.content + str(num) + ".png"
|
|
num += 1
|
|
try:
|
|
with open(temp_file_name, "wb") as dalle_file:
|
|
dalle_file.write(response.content)
|
|
except OSError:
|
|
temp_file_name = "/home/pi/oserror.png"
|
|
with open(temp_file_name, "wb") as dalle_file:
|
|
dalle_file.write(response.content)
|
|
except FileNotFoundError:
|
|
temp_file_name = "/home/pi/fnferror.png"
|
|
with open(temp_file_name, "wb") as dalle_file:
|
|
dalle_file.write(response.content)
|
|
except Exception as e:
|
|
self.logger.error("Nieznany błąd: %s", e)
|
|
temp_file_name = "/home/pi/error.png"
|
|
with open(temp_file_name, "wb") as dalle_file:
|
|
dalle_file.write(response.content)
|
|
finally:
|
|
self.logger.info("Koniec procedury obrazkowej.")
|
|
fnord = discord.File(
|
|
temp_file_name, spoiler=False, description=message.content
|
|
)
|
|
|
|
#await message.reply(f"{image_desc}", file=fnord)
|
|
await discord_friendly_reply(message,f"{image_desc}", file = fnord)
|
|
# *=========================================== Define Functions
|
|
|
|
|
|
async def setup(bot):
|
|
logger = logging.getLogger("discord")
|
|
await bot.add_cog(Events(bot))
|
|
logger.info("Loading ai events module done")
|