mirror of
https://github.com/migatu/conjurer.git
synced 2026-07-14 21:38:38 +00:00
3d9d47aa90
Wire the bot's AI chat pipeline (ai_functions.handle_response) to talk to either OpenAI or the Anthropic Messages API, chosen by one active-config switch. Behaviour on the default "gpt" config is unchanged. constants.py: * guarded `import anthropic` + CLAUDECLIENT (mirrors OPENAICLIENT), netrc machine 'anthropic' / ANTHROPIC_API_KEY; * CLAUDE_LATEST_MODEL / CLAUDE_CHEAP_MODEL (opus-4-8 / haiku-4-5); * AI_CONFIGS + DEFAULT_AI_CONFIG loaded from an optional 3rd element of system_gpt_settings.json (backward compatible - a 2-element file falls back to built-in defaults, active "gpt"). Single switch: CONJURER_AI_CONFIG env > settings "active" > "gpt". ai_functions.py: * provider_generate() dispatches to OpenAI (unchanged openai_call) or the new _anthropic_call() (splits system out, alternating messages, max_tokens, temperature omitted - Opus 4.8 rejects sampling params); * AIError normalises both SDKs' exceptions into one category set so handle_response keeps its single set of in-character error replies; * select_model() reads the active config; legacy "gpt-4o" default auto-maps to the active provider's model so the switch actually changes the backend; * set_active_ai_config()/list_ai_configs() with best-effort persistence back into system_gpt_settings.json index 2. ai_commands.py: * $gadaj_teraz <config> hybrid command (Vykidailo-gated) switches backend at runtime; * graceful guards when OPENAICLIENT is None: personal assistants (OpenAI Assistants API) and DALL-E image gen degrade instead of crashing, so a Claude-only deployment boots. system_gpt_settings.json: add the configs block (gpt/claude/_template) as the collection point for future backends. requirements_bot.txt: add anthropic. bot.env.example: ANTHROPIC_API_KEY + CONJURER_AI_CONFIG. Unit tests cover the message splitter, model selection, config listing, and error mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
645 lines
26 KiB
Python
645 lines
26 KiB
Python
import asyncio
|
||
import json
|
||
import logging
|
||
import random
|
||
|
||
import openai
|
||
import tiktoken
|
||
import time
|
||
from other_functions import discord_friendly_send
|
||
from constants import (
|
||
AI_CONFIGS,
|
||
ASSISTANTS,
|
||
CLAUDECLIENT,
|
||
CYCLIC_WORDS,
|
||
DEFAULT_AI_CONFIG,
|
||
ENCODING,
|
||
GPT_SETTINGS,
|
||
MEMORY_FIVE_MUZYKA,
|
||
MEMORY_FIVE_SIARA,
|
||
MESSAGE_TABLE,
|
||
MESSAGE_TABLE_MUZYKA,
|
||
OPENAICLIENT,
|
||
SYSTEM_GPT_SETTINGS,
|
||
WORD_REACTIONS,
|
||
CHEAP_MODEL,
|
||
LATEST_MODEL
|
||
)
|
||
|
||
try:
|
||
import anthropic
|
||
except ImportError: # pragma: no cover - optional at runtime
|
||
anthropic = None
|
||
|
||
# this do per user
|
||
VECTOR_STORE_ID = -1
|
||
|
||
|
||
# *=========================================== AI provider abstraction
|
||
# The AI cog talks to exactly one backend at a time, chosen by _ACTIVE_CONFIG.
|
||
# Legacy defaults ("gpt"/OpenAI) keep the historical behaviour byte-for-byte;
|
||
# selecting a "claude" config routes the same handle_response pipeline through
|
||
# the Anthropic Messages API instead. Backend-specific exceptions are funnelled
|
||
# into a single AIError so handle_response can keep its one set of in-character
|
||
# error replies regardless of provider.
|
||
_ACTIVE_CONFIG_NAME = DEFAULT_AI_CONFIG
|
||
|
||
# Legacy default algorithm strings that mean "let the bot pick" rather than
|
||
# "force this exact model" - so a caller that still passes the old gpt-4o
|
||
# default auto-selects the active provider's model instead of 400-ing on Claude.
|
||
_AUTO_ALGOS = {"", "auto", "gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"}
|
||
|
||
|
||
class AIError(Exception):
|
||
"""Provider-neutral wrapper so handle_response reacts to one exception type.
|
||
|
||
``category`` is one of: timeout, connection, bad_request,
|
||
response_validation, auth, permission, rate_limit, unprocessable, api.
|
||
``original`` is the underlying SDK exception (interpolated into replies).
|
||
"""
|
||
|
||
def __init__(self, category: str, original: Exception):
|
||
super().__init__(str(original))
|
||
self.category = category
|
||
self.original = original
|
||
|
||
|
||
def _active_config() -> dict:
|
||
return (
|
||
AI_CONFIGS.get(_ACTIVE_CONFIG_NAME)
|
||
or AI_CONFIGS.get("gpt")
|
||
or next(iter(AI_CONFIGS.values()))
|
||
)
|
||
|
||
|
||
def list_ai_configs():
|
||
"""Selectable config names (templates prefixed with '_' are hidden)."""
|
||
return [name for name in AI_CONFIGS if not name.startswith("_")]
|
||
|
||
|
||
def get_active_ai_config() -> str:
|
||
return _ACTIVE_CONFIG_NAME
|
||
|
||
|
||
def set_active_ai_config(name: str) -> dict:
|
||
"""Switch the active AI backend and persist the choice. Raises on error."""
|
||
global _ACTIVE_CONFIG_NAME
|
||
if name not in AI_CONFIGS:
|
||
raise KeyError(name)
|
||
cfg = AI_CONFIGS[name]
|
||
provider = cfg.get("provider")
|
||
if provider == "anthropic" and CLAUDECLIENT is None:
|
||
raise RuntimeError("klient Anthropic nie jest skonfigurowany (brak ANTHROPIC_API_KEY)")
|
||
if provider == "openai" and OPENAICLIENT is None:
|
||
raise RuntimeError("klient OpenAI nie jest skonfigurowany (brak OPENAI_API_KEY)")
|
||
_ACTIVE_CONFIG_NAME = name
|
||
_persist_active_ai_config(name)
|
||
return cfg
|
||
|
||
|
||
def _persist_active_ai_config(name: str) -> None:
|
||
"""Best-effort write of the active-config choice into system_gpt_settings.json.
|
||
|
||
Keeps the historical two-element structure intact: updates index 2 if it
|
||
already exists, appends it when the file has exactly the original two
|
||
elements, and otherwise leaves the file untouched (the in-memory switch
|
||
still applies).
|
||
"""
|
||
logger = logging.getLogger("discord")
|
||
try:
|
||
with open(SYSTEM_GPT_SETTINGS, "r", encoding=ENCODING) as handle:
|
||
data = json.load(handle)
|
||
except (OSError, json.JSONDecodeError) as exc:
|
||
logger.warning("Nie mogę odczytać %s do zapisu configu AI: %s", SYSTEM_GPT_SETTINGS, exc)
|
||
return
|
||
if not isinstance(data, list) or len(data) < 2:
|
||
logger.warning("Nietypowa struktura %s - pomijam zapis configu AI", SYSTEM_GPT_SETTINGS)
|
||
return
|
||
if len(data) > 2 and isinstance(data[2], dict):
|
||
data[2]["active"] = name
|
||
data[2].setdefault("configs", AI_CONFIGS)
|
||
else:
|
||
data = data[:2] + [{"active": name, "configs": AI_CONFIGS}]
|
||
try:
|
||
with open(SYSTEM_GPT_SETTINGS, "w", encoding=ENCODING) as handle:
|
||
json.dump(data, handle, indent=4, ensure_ascii=False)
|
||
except OSError as exc:
|
||
logger.warning("Nie mogę zapisać configu AI do %s: %s", SYSTEM_GPT_SETTINGS, exc)
|
||
|
||
|
||
def _map_openai_error(exc: Exception) -> AIError:
|
||
mapping = [
|
||
(openai.APITimeoutError, "timeout"),
|
||
(openai.APIConnectionError, "connection"),
|
||
(openai.BadRequestError, "bad_request"),
|
||
(openai.APIResponseValidationError, "response_validation"),
|
||
(openai.AuthenticationError, "auth"),
|
||
(openai.PermissionDeniedError, "permission"),
|
||
(openai.RateLimitError, "rate_limit"),
|
||
(openai.UnprocessableEntityError, "unprocessable"),
|
||
(openai.APIError, "api"),
|
||
]
|
||
for cls, category in mapping:
|
||
if isinstance(exc, cls):
|
||
return AIError(category, exc)
|
||
return AIError("api", exc)
|
||
|
||
|
||
def _map_anthropic_error(exc: Exception) -> AIError:
|
||
mapping = [
|
||
("APITimeoutError", "timeout"),
|
||
("APIConnectionError", "connection"),
|
||
("BadRequestError", "bad_request"),
|
||
("APIResponseValidationError", "response_validation"),
|
||
("AuthenticationError", "auth"),
|
||
("PermissionDeniedError", "permission"),
|
||
("RateLimitError", "rate_limit"),
|
||
("UnprocessableEntityError", "unprocessable"),
|
||
("APIError", "api"),
|
||
]
|
||
for name, category in mapping:
|
||
cls = getattr(anthropic, name, None)
|
||
if cls and isinstance(exc, cls):
|
||
return AIError(category, exc)
|
||
return AIError("api", exc)
|
||
|
||
|
||
def _to_anthropic_messages(messages):
|
||
"""Split OpenAI-style messages into (system_prompt, alternating convo).
|
||
|
||
Claude takes the system prompt as a separate parameter (not a role in the
|
||
messages list) and requires the conversation to open with a user turn, so
|
||
system messages are concatenated out and any leading assistant turns are
|
||
dropped.
|
||
"""
|
||
system_parts = []
|
||
convo = []
|
||
for msg in messages:
|
||
role = msg.get("role")
|
||
content = msg.get("content", "")
|
||
if role == "system":
|
||
system_parts.append(content)
|
||
else:
|
||
convo.append(
|
||
{"role": "assistant" if role == "assistant" else "user", "content": content}
|
||
)
|
||
while convo and convo[0]["role"] != "user":
|
||
convo.pop(0)
|
||
if not convo:
|
||
convo = [{"role": "user", "content": " "}]
|
||
return "\n\n".join(part for part in system_parts if part), convo
|
||
|
||
|
||
async def _anthropic_call(messages, model, cfg):
|
||
"""Claude counterpart of openai_call. Returns a plain string."""
|
||
if CLAUDECLIENT is None:
|
||
raise AIError("auth", RuntimeError("klient Anthropic nie jest skonfigurowany"))
|
||
system_prompt, convo = _to_anthropic_messages(messages)
|
||
kwargs = {
|
||
"model": model,
|
||
"max_tokens": int(cfg.get("max_tokens", 2048)),
|
||
"messages": convo,
|
||
}
|
||
if system_prompt:
|
||
kwargs["system"] = system_prompt
|
||
# NOTE: temperature is deliberately omitted - Opus 4.8 / Sonnet 5 reject
|
||
# sampling params with a 400.
|
||
try:
|
||
resp = await CLAUDECLIENT.messages.create(**kwargs)
|
||
except Exception as exc: # pylint: disable=broad-except
|
||
raise _map_anthropic_error(exc)
|
||
text = "".join(
|
||
block.text for block in resp.content if getattr(block, "type", None) == "text"
|
||
)
|
||
return text.strip()
|
||
|
||
|
||
async def provider_generate(messages, model, temperature=0.2):
|
||
"""Dispatch a chat completion to the active backend, normalising errors."""
|
||
cfg = _active_config()
|
||
try:
|
||
if cfg.get("provider") == "anthropic":
|
||
return await _anthropic_call(messages, model, cfg)
|
||
return await openai_call(messages, model, temperature)
|
||
except AIError:
|
||
raise
|
||
except Exception as exc: # pylint: disable=broad-except
|
||
# Only the OpenAI path reaches here un-normalised (_anthropic_call
|
||
# already wraps its own errors).
|
||
raise _map_openai_error(exc)
|
||
|
||
|
||
def select_model(req_type: str, algo: str) -> str:
|
||
cfg = _active_config()
|
||
latest = cfg.get("latest_model", LATEST_MODEL)
|
||
cheap = cfg.get("cheap_model", CHEAP_MODEL)
|
||
algo_str = (algo or "").strip()
|
||
# An explicit, non-legacy model id is honoured verbatim; anything in
|
||
# _AUTO_ALGOS (incl. the old gpt-4o default) means "auto pick for the
|
||
# active provider", so flipping the switch actually changes the model.
|
||
if algo_str and algo_str.lower() not in _AUTO_ALGOS:
|
||
return algo_str
|
||
if req_type == "MUSIC":
|
||
return cheap
|
||
return latest
|
||
|
||
|
||
async def openai_call(messages, model, temperature=0.2):
|
||
"""
|
||
Responses API dla 4o/4.1*, fallback Chat Completions dla gpt-3.5-turbo.
|
||
Zwraca czysty string odpowiedzi.
|
||
"""
|
||
logger = logging.getLogger("discord")
|
||
|
||
if model.startswith("gpt-3.5"):
|
||
logger.info("3.5")
|
||
# legacy path – bez zmian w Twoim kodzie wyżej/niżej
|
||
resp = await OPENAICLIENT.chat.completions.create(
|
||
model=model,
|
||
messages=messages,
|
||
temperature=temperature,
|
||
)
|
||
result = ""
|
||
for choice in resp.choices:
|
||
result += choice.message.content
|
||
return result.strip()
|
||
|
||
else:
|
||
logger.info("4.0+")
|
||
# Responses API (zalecane dla 4o/4.1*)
|
||
resp = await OPENAICLIENT.responses.create(
|
||
model=model,
|
||
temperature=temperature,
|
||
input=messages,
|
||
)
|
||
# SDK zapewnia output_text dla zwykłych odpowiedzi
|
||
return (getattr(resp.output_text, "output_text", None) or "").strip() or str(
|
||
resp.output_text
|
||
)
|
||
|
||
|
||
def create_vector_store():
|
||
# Create a vector store caled "Financial Statements"
|
||
return OPENAICLIENT.beta.vector_stores.create_and_poll(name="Hammer Stash")
|
||
# expires_after={
|
||
# "anchor": "last_active_at",
|
||
# "days": 7}
|
||
# )
|
||
|
||
|
||
def upload_files_to_vector_store(assistant):
|
||
|
||
# Ready the files for upload to OpenAI
|
||
file_paths = ["edgar/goog-10k.pdf", "edgar/brka-10k.txt"]
|
||
file_streams = [open(path, "rb") for path in file_paths]
|
||
|
||
# file = client.beta.vector_stores.files.create_and_poll(
|
||
# vector_store_id="vs_abc123",
|
||
# file_id="file-abc123"
|
||
# )
|
||
# batch = client.beta.vector_stores.file_batches.create_and_poll(
|
||
# vector_store_id="vs_abc123",
|
||
# file_ids=['file_1', 'file_2', 'file_3', 'file_4', 'file_5']
|
||
# )
|
||
|
||
# Use the upload and poll SDK helper to upload the files, add them to the vector store,
|
||
# and poll the status of the file batch for completion.
|
||
file_batch = OPENAICLIENT.beta.vector_stores.file_batches.upload_and_poll(
|
||
vector_store_id=VECTOR_STORE_ID, files=file_streams
|
||
)
|
||
|
||
# You can print the status and the file counts of the batch to see the result of this operation.
|
||
print(file_batch.status)
|
||
print(file_batch.file_counts)
|
||
assistant = OPENAICLIENT.beta.assistants.update(
|
||
assistant_id=assistant.id,
|
||
tool_resources={"file_search": {"vector_store_ids": [VECTOR_STORE_ID]}},
|
||
)
|
||
|
||
|
||
def delete_files_from_vector_store(assistant, file_id):
|
||
result = OPENAICLIENT.beta.vector_stores.file_batches.delete(
|
||
vector_store_id=VECTOR_STORE_ID, files=file_id
|
||
)
|
||
|
||
# You can print the status and the file counts of the batch to see the result of this operation.
|
||
print(result)
|
||
assistant = OPENAICLIENT.beta.assistants.update(
|
||
assistant_id=assistant.id,
|
||
tool_resources={"file_search": {"vector_store_ids": [VECTOR_STORE_ID]}},
|
||
)
|
||
|
||
|
||
def num_tokens_from_string(message, model):
|
||
"""
|
||
The function takes a string message and a model as input and returns the number of tokens in the
|
||
message according to the given model.
|
||
|
||
:param message: A string containing the message or text from which you want to count the number of
|
||
tokens
|
||
:param model: The model parameter refers to a language model or tokenizer that can be used to
|
||
tokenize the input string. It could be a pre-trained model or a custom tokenizer
|
||
"""
|
||
tokens_per_message = 3
|
||
tokens_per_name = 1
|
||
chat_gpt_encoding = tiktoken.encoding_for_model(model)
|
||
|
||
num_tokens = 0
|
||
num_tokens += tokens_per_message
|
||
for keys, values in message.items():
|
||
num_tokens += len(chat_gpt_encoding.encode(values))
|
||
if keys == "role":
|
||
num_tokens += tokens_per_name
|
||
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
||
return num_tokens
|
||
|
||
|
||
async def handle_response(
|
||
prompt,
|
||
vykidailo,
|
||
bartender,
|
||
history,
|
||
username,
|
||
request_type,
|
||
algorithm="gpt-4o",
|
||
none_request="",
|
||
internal_retry: bool = False
|
||
):
|
||
"""
|
||
Handle responses by appending them to a history, use OpenAI to
|
||
generate a response, and then append the generated response to the history.
|
||
|
||
:param prompt: The prompt for the OpenAI chatbot to generate a response to
|
||
:param vykidailo: It is a boolean variable that indicates whether the user invoking the function is
|
||
an administrator or not
|
||
:param bartender: The bartender parameter is a boolean value indicating whether the user making the
|
||
request is a bartender or not
|
||
:param history: A list containing the conversation history between the user and the assistant
|
||
:param username: The username of the user who initiated the conversation
|
||
:param music: The "music" parameter is a boolean value that indicates whether the conversation is
|
||
related to music or not. If it is True, the conversation history will be stored in a different file
|
||
and the response will be generated using a different model
|
||
:param request_type: The type of request being made, which can be "MUSIC", "RANDOM", "NONE" or "GENERAL"
|
||
GENERAL is for regular conversations, MUSIC is for music-related requests,
|
||
RANDOM is for random requests, and NONE is to not store the request in memory
|
||
:param algorithm: The algorithm to be used for generating the response, default is "gpt-4o"
|
||
:return: The function `handle_response` returns a tuple containing the `result` and `MESSAGE_TABLE`.
|
||
"""
|
||
logger = logging.getLogger("discord")
|
||
logger.info("Wywolanie procedury openai z promptem: %s", prompt)
|
||
if vykidailo or bartender:
|
||
logger.info("Administrator coś chciał")
|
||
model_to_use = select_model(request_type, algorithm)
|
||
logger.info("Wybrany model: %s", model_to_use)
|
||
if request_type == "MUSIC" and model_to_use == "gpt-4o-mini":
|
||
try:
|
||
# nic — normalnie pójdzie Responses API
|
||
pass
|
||
except Exception:
|
||
model_to_use = "gpt-3.5-turbo"
|
||
# --- 2) Budowa historii (token budget + reguły systemowe) ---
|
||
# NOTE: ignorujemy przekazany 'history' jako listę (tak było wcześniej),
|
||
# ale zwracamy aktualną tablicę do nadpisania w miejscach wołania (back-compat).
|
||
base_system = GPT_SETTINGS[0] # zakładamy {"role":"system","content":...}
|
||
history_msgs = []
|
||
|
||
if request_type != "NONE":
|
||
history_msgs.append(base_system)
|
||
chat_gpt_config_request_size = num_tokens_from_string(base_system, "gpt-4")
|
||
|
||
# Dynamiczne mikro-reguły (WORD_REACTIONS), jak w Twoim kodzie
|
||
for slowo, reakcja in WORD_REACTIONS.items():
|
||
if not reakcja[3]:
|
||
content = f"Kiedy słyszysz {slowo} to reagujesz lub dzieje się to {reakcja[0]}"
|
||
sys_msg = {"role": "system", "content": content}
|
||
chat_gpt_config_request_size += num_tokens_from_string(sys_msg, "gpt-4")
|
||
history_msgs.append(sys_msg)
|
||
|
||
# wybór właściwej tablicy pamięci i budżetu
|
||
if request_type == "MUSIC":
|
||
table = MESSAGE_TABLE_MUZYKA
|
||
token_amount = 10700
|
||
elif request_type in ("RANDOM", "GENERAL"):
|
||
table = MESSAGE_TABLE
|
||
token_amount = 10700
|
||
else:
|
||
table = []
|
||
token_amount = 10000
|
||
|
||
# doklejanie historii od końca aż do limitu (zachowana kolejność czasowa)
|
||
final_prompt = f"{username}:{prompt}"
|
||
prompt_gpt_request_size = num_tokens_from_string({"role": "user", "content": final_prompt}, "gpt-4")
|
||
|
||
acc = []
|
||
for msg in reversed(table):
|
||
t = num_tokens_from_string(msg, "gpt-4")
|
||
if chat_gpt_config_request_size + prompt_gpt_request_size + t <= token_amount:
|
||
acc.append(msg)
|
||
chat_gpt_config_request_size += t
|
||
else:
|
||
break
|
||
# przywróć chronologicznie
|
||
history_msgs.extend(reversed(acc))
|
||
|
||
# aktualny prompt
|
||
history_msgs.append({"role": "user", "content": final_prompt})
|
||
logger.info("Rozmiar zapytania (tok): %s", prompt_gpt_request_size) # tokeny już policzone wyżej
|
||
|
||
else:
|
||
# --- tryb NONE: nie dotykamy pamięci i pozwalamy przekazać własny 'none_request' ---
|
||
if isinstance(none_request, list):
|
||
history_msgs = none_request
|
||
elif isinstance(none_request, str) and none_request.strip():
|
||
history_msgs = [{"role": "user", "content": none_request}]
|
||
else:
|
||
history_msgs = [{"role": "user", "content": f"{username}:{prompt}"}]
|
||
|
||
logger.info("Rozmiar zapytania (tok): %s", "n/a") # tokeny już policzone wyżej
|
||
|
||
try:
|
||
# ...przygotowanie messages/system prompt/itp. jak masz...
|
||
# retry/backoff + deadline (zachowuje Twoją semantykę logowania)
|
||
timeout_sec = 120
|
||
deadline = time.time() + timeout_sec
|
||
response = await asyncio.wait_for(
|
||
provider_generate(messages=history_msgs, model=model_to_use),
|
||
timeout=max(0.1, deadline - time.time()),
|
||
)
|
||
|
||
except AIError as e:
|
||
# One handler for both backends; e.category is provider-neutral and
|
||
# e.original is the underlying SDK exception (kept for the {..} tails).
|
||
err = e.original
|
||
if e.category == "timeout":
|
||
response = 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ę*: {err}"
|
||
elif e.category == "connection":
|
||
response = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {err}"
|
||
elif e.category in ("bad_request", "response_validation"):
|
||
# Handle invalid request error, e.g. validate parameters or log
|
||
if internal_retry:
|
||
resp = "Nie umiem tego teraz ładnie wytłumaczyć — OpenAI mnie zastrzeliło."
|
||
else:
|
||
resp, _ = await handle_response(
|
||
f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {err} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'",
|
||
True,
|
||
True,
|
||
MESSAGE_TABLE,
|
||
username,
|
||
"RANDOM",
|
||
internal_retry=True,
|
||
)
|
||
response = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {err}"
|
||
elif e.category == "auth":
|
||
# Handle authentication error, e.g. check credentials or log
|
||
response = 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ę:* {err}"
|
||
elif e.category == "permission":
|
||
# Handle permission error, e.g. check scope or log
|
||
response = 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ę:* {err}"
|
||
elif e.category == "rate_limit":
|
||
response = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {err}"
|
||
elif e.category == "unprocessable":
|
||
response = f"*Kondziu patrzy na terminal. Potem na to co każesz mu wysłać....* Ja wiem że jesteśmy w barze BDSM - ale nie da się włożyć TEGO w TO. *Za jego plecami na terminalu pojawia się:* {err}"
|
||
else: # "api" and anything unmapped
|
||
# Handle API error, e.g. retry or log
|
||
response = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {err}"
|
||
|
||
logger.info("Historia wysłana:")
|
||
temp_assistant = {"role": "assistant", "content": response}
|
||
logger.info(temp_assistant)
|
||
if request_type == "MUSIC":
|
||
# zapis do pliku MUZYKA
|
||
with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as fh:
|
||
file_data = json.load(fh)
|
||
file_data.append({"role": "user", "content": f"{username}:{prompt}"})
|
||
file_data.append(temp_assistant)
|
||
fh.seek(0)
|
||
json.dump(file_data, fh, indent=4)
|
||
return response, MESSAGE_TABLE_MUZYKA
|
||
|
||
elif request_type in ("RANDOM", "GENERAL"):
|
||
with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as fh:
|
||
file_data = json.load(fh)
|
||
file_data.append({"role": "user", "content": f"{username}:{prompt}"})
|
||
file_data.append(temp_assistant)
|
||
fh.seek(0)
|
||
json.dump(file_data, fh, indent=4)
|
||
return response, MESSAGE_TABLE
|
||
|
||
else: # NONE
|
||
return response, []
|
||
|
||
|
||
async def get_random_cyclic_message(client):
|
||
"""
|
||
The function `get_random_cyclic_message` returns a random cyclic message from a list of cyclic
|
||
words.
|
||
:return: a random cyclic message from the list `cyclic_words`.
|
||
"""
|
||
logger = logging.getLogger("discord")
|
||
channel_id = 1062047367337095268
|
||
channel = client.get_channel(channel_id)
|
||
# trunk-ignore(bandit/B311)
|
||
ai_check = random.randint(0, 10)
|
||
logger.info("Losowa wypowiedź")
|
||
if ai_check < 2:
|
||
logger.info("Predefiniowana")
|
||
# trunk-ignore(bandit/B311)
|
||
messnum = random.randint(0, len(CYCLIC_WORDS))
|
||
logger.debug(messnum)
|
||
logger.debug(len(CYCLIC_WORDS))
|
||
mess_key = list(CYCLIC_WORDS.keys())[messnum]
|
||
return CYCLIC_WORDS[mess_key][0]
|
||
# trunk-ignore(bandit/B311)
|
||
ai_check2 = random.randint(0, 10)
|
||
global MESSAGE_TABLE
|
||
if ai_check2 < 6:
|
||
logger.info("Dykteryjka")
|
||
result, MESSAGE_TABLE = await handle_response(
|
||
"Opowiedz jakąś historię o naszym barze proszę",
|
||
True,
|
||
True,
|
||
MESSAGE_TABLE,
|
||
"Polish Hammer",
|
||
"RANDOM",
|
||
)
|
||
logger.info(result)
|
||
else:
|
||
logger.info("Wtracenie w dyskusje")
|
||
messages = [message async for message in channel.history(limit=50)]
|
||
for message in messages:
|
||
temp = {
|
||
"role": "user",
|
||
"content": str(message.author) + ":" + str(message.content),
|
||
}
|
||
MESSAGE_TABLE.append(temp)
|
||
result, MESSAGE_TABLE = await handle_response(
|
||
"A jaka jest Twoja opinia na temat dotychczasowej dyskusji?",
|
||
True,
|
||
True,
|
||
MESSAGE_TABLE,
|
||
"Polish Hammer",
|
||
"RANDOM",
|
||
)
|
||
logger.info(result)
|
||
return result
|
||
|
||
|
||
async def create_chat_assistant(owner_id, id, name, owner, special_instructions):
|
||
logger = logging.getLogger("discord")
|
||
instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o."
|
||
instruction += special_instructions
|
||
assistant = await OPENAICLIENT.beta.assistants.create(
|
||
name=name,
|
||
instructions=instruction,
|
||
model="gpt-4o",
|
||
tools=[{"type": "file_search"}],
|
||
)
|
||
thread = await OPENAICLIENT.beta.threads.create()
|
||
logger.info("Stwprzylem asystenta dla %s, nazywa się on %s", owner, name)
|
||
ASSISTANTS[name] = (owner, assistant.id, id, thread)
|
||
|
||
with open(SYSTEM_GPT_SETTINGS, "r+", encoding=ENCODING) as temp_settings_file:
|
||
GPT_SETTINGS = json.load(temp_settings_file)
|
||
GPT_SETTINGS[1][owner_id][4] = assistant.id
|
||
temp_settings_file.seek(0)
|
||
json.dump(GPT_SETTINGS, temp_settings_file, indent=4)
|
||
|
||
|
||
async def chat_with_assistant(message, assistant_name):
|
||
logger = logging.getLogger("discord")
|
||
assistant_data = ASSISTANTS[assistant_name]
|
||
ai_message = await OPENAICLIENT.beta.threads.messages.create(
|
||
thread_id=assistant_data[3].id, role="user", content=message.content
|
||
)
|
||
logger.info(ai_message)
|
||
run = await OPENAICLIENT.beta.threads.runs.create_and_poll(
|
||
thread_id=assistant_data[3].id,
|
||
assistant_id=assistant_data[1],
|
||
instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy",
|
||
)
|
||
done = False
|
||
while not done:
|
||
if run.status == "completed":
|
||
messsages = await OPENAICLIENT.beta.threads.messages.list(
|
||
thread_id=assistant_data[3].id
|
||
)
|
||
logger.info(messsages)
|
||
reply_content = messsages.data[0].content
|
||
logger.info(reply_content)
|
||
chat_response = ""
|
||
for block in reply_content:
|
||
logger.info(block.text.value)
|
||
chat_response += block.text.value
|
||
await discord_friendly_send(message.channel, chat_response)
|
||
# await message.channel.send(chat_response)
|
||
done = True
|
||
elif run.status == "cancelled":
|
||
await discord_friendly_send(message.channel, "Cos sie wywaliło")
|
||
else:
|
||
logger.info(run.status)
|
||
asyncio.sleep(5)
|
||
|
||
|
||
async def echo(message):
|
||
await discord_friendly_send(message.channel, f"Echo: {message.content}")
|