Latex incoming changes

This commit is contained in:
2025-08-16 17:54:06 +02:00
parent 6cc43ed16d
commit f0bd6c4015
4 changed files with 174 additions and 113 deletions
+63 -60
View File
@@ -133,7 +133,7 @@ def num_tokens_from_string(message, model):
async def handle_response( async def handle_response(
prompt, vykidailo, bartender, history, username, request_type, algorithm="gpt-4o" prompt, vykidailo, bartender, history, username, request_type, algorithm="gpt-4o", none_request=""
): ):
""" """
Handle responses by appending them to a history, use OpenAI to Handle responses by appending them to a history, use OpenAI to
@@ -189,60 +189,63 @@ async def handle_response(
# convert back to json. # convert back to json.
json.dump(file_data, file_memory, indent=4) json.dump(file_data, file_memory, indent=4)
history = [] history = []
history.append(GPT_SETTINGS[0]) if request_type != "NONE":
chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4") history.append(GPT_SETTINGS[0])
chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4")
for slowo, reakcja in WORD_REACTIONS.items(): for slowo, reakcja in WORD_REACTIONS.items():
if not reakcja[3]: if not reakcja[3]:
content = ( content = (
"Kiedy słyszysz " "Kiedy słyszysz "
+ slowo + slowo
+ " to reagujesz lub dzieje się to " + " to reagujesz lub dzieje się to "
+ reakcja[0] + reakcja[0]
) )
temp = {"role": "system", "content": content} temp = {"role": "system", "content": content}
chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4")
history.append(temp) history.append(temp)
final_prompt = username + ":" + prompt final_prompt = username + ":" + prompt
logger.debug(
"Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size
)
if request_type == "MUSIC":
table = MESSAGE_TABLE_MUZYKA
token_amount = 10700
elif request_type == "RANDOM":
table = MESSAGE_TABLE
token_amount = 10700
elif request_type == "GENERAL":
table = MESSAGE_TABLE
token_amount = 10700
else:
table = []
token_amount = 10000
prompt_gpt_request_size = num_tokens_from_string(
{"role": "user", "content": final_prompt}, "gpt-4"
)
temptable = []
for i in reversed(table):
temp_token = num_tokens_from_string(i, "gpt-4")
logger.debug( logger.debug(
"Rozmiar zapytania %s prompt %s temp %s", "Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size
chat_gpt_config_request_size,
prompt_gpt_request_size,
temp_token,
) )
if ( if request_type == "MUSIC":
chat_gpt_config_request_size table = MESSAGE_TABLE_MUZYKA
< token_amount + prompt_gpt_request_size + temp_token token_amount = 10700
): elif request_type == "RANDOM":
temptable.insert(1, i) table = MESSAGE_TABLE
chat_gpt_config_request_size += temp_token token_amount = 10700
history.extend(temptable) elif request_type == "GENERAL":
temp = {"role": "user", "content": final_prompt} table = MESSAGE_TABLE
history.append(temp) token_amount = 10700
else:
table = []
token_amount = 10000
prompt_gpt_request_size = num_tokens_from_string(
{"role": "user", "content": final_prompt}, "gpt-4"
)
temptable = []
for i in reversed(table):
temp_token = num_tokens_from_string(i, "gpt-4")
logger.debug(
"Rozmiar zapytania %s prompt %s temp %s",
chat_gpt_config_request_size,
prompt_gpt_request_size,
temp_token,
)
if (
chat_gpt_config_request_size
< token_amount + prompt_gpt_request_size + temp_token
):
temptable.insert(1, i)
chat_gpt_config_request_size += temp_token
history.extend(temptable)
temp = {"role": "user", "content": final_prompt}
history.append(temp)
else:
history = none_request
logger.info("Rozmiar zapytania po wyslaniu %s", chat_gpt_config_request_size) logger.info("Rozmiar zapytania po wyslaniu %s", chat_gpt_config_request_size)
try: try:
# ...przygotowanie messages/system prompt/itp. jak masz... # ...przygotowanie messages/system prompt/itp. jak masz...
@@ -256,9 +259,9 @@ async def handle_response(
except openai.APITimeoutError as e: except openai.APITimeoutError as e:
# Handle timeout error, e.g. retry or log # Handle timeout error, e.g. retry or log
result = 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}" 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ę*: {e}"
except openai.APIConnectionError as e: except openai.APIConnectionError as e:
result = 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}" 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ę*: {e}"
except openai.BadRequestError as e: except openai.BadRequestError as e:
# Handle invalid request error, e.g. validate parameters or log # Handle invalid request error, e.g. validate parameters or log
resp, _ = await handle_response( resp, _ = await handle_response(
@@ -269,7 +272,7 @@ async def handle_response(
username, username,
"RANDOM", "RANDOM",
) )
result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" response = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}"
except openai.APIResponseValidationError as e: except openai.APIResponseValidationError as e:
# Handle invalid request error, e.g. validate parameters or log # Handle invalid request error, e.g. validate parameters or log
resp, _ = await handle_response( resp, _ = await handle_response(
@@ -280,20 +283,20 @@ async def handle_response(
username, username,
"RANDOM", "RANDOM",
) )
result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" response = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}"
except openai.AuthenticationError as e: except openai.AuthenticationError as e:
# Handle authentication error, e.g. check credentials or log # Handle authentication error, e.g. check credentials or log
result = 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}" 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ę:* {e}"
except openai.PermissionDeniedError as e: except openai.PermissionDeniedError as e:
# Handle permission error, e.g. check scope or log # Handle permission error, e.g. check scope or log
result = 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}" 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ę:* {e}"
except openai.RateLimitError as e: except openai.RateLimitError as e:
result = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" response = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}"
except openai.UnprocessableEntityError as e: except openai.UnprocessableEntityError as e:
result = 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ę:* {e}" 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ę:* {e}"
except openai.APIError as e: except openai.APIError as e:
# Handle API error, e.g. retry or log # Handle API error, e.g. retry or log
result = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" response = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}"
logger.info("Historia wysłana:") logger.info("Historia wysłana:")
logger.info(history) logger.info(history)
+4 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import io import io
from typing import Optional, List from typing import Optional, List
import logging
import discord import discord
from discord import app_commands from discord import app_commands
@@ -128,5 +129,7 @@ class LatexModule(commands.Cog):
self.logger.debug("BATCH failed: %s", ", ".join(failed)) self.logger.debug("BATCH failed: %s", ", ".join(failed))
await interaction.followup.send("✅ Gotowe (ZIP w załączniku)." + (f" ❌ Błędy: {len(failed)}" if failed else ""), files=[discord.File(bio, filename=bio.name)]) await interaction.followup.send("✅ Gotowe (ZIP w załączniku)." + (f" ❌ Błędy: {len(failed)}" if failed else ""), files=[discord.File(bio, filename=bio.name)])
async def setup(bot: commands.Bot): async def setup(bot):
logger = logging.getLogger("discord")
await bot.add_cog(LatexModule(bot, logger_name="latex")) await bot.add_cog(LatexModule(bot, logger_name="latex"))
logger.info("Loading kinky latex module done")
+106 -52
View File
@@ -1,76 +1,122 @@
# latex_functions.py # latex_functions.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
import asyncio, os, re, shlex, tempfile, zipfile
from pathlib import Path
from typing import Tuple, List, Dict, Optional
from openai import OpenAI import asyncio
import re
import shlex
import tempfile
import zipfile
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from ai_functions import handle_response
SAFE_TEX_NAME = re.compile(r"^[\w\-. ]+\.tex$", re.IGNORECASE) SAFE_TEX_NAME = re.compile(r"^[\w\-. ]+\.tex$", re.IGNORECASE)
def is_safe_tex_name(name: str) -> bool: def is_safe_tex_name(name: str) -> bool:
return bool(SAFE_TEX_NAME.match(name or "")) and name.lower().endswith(".tex") return bool(SAFE_TEX_NAME.match(name or "")) and name.lower().endswith(".tex")
def is_tex_attachment(att, max_mb: int) -> bool: def is_tex_attachment(att, max_mb: int) -> bool:
"""Filtruje discord.Attachment: tylko .tex + limit MB + nazwa bezpieczna.""" """Filtruje discord.Attachment: tylko .tex + limit MB + nazwa bezpieczna."""
return bool( return bool(
getattr(att, "filename", "") and getattr(att, "filename", "")
is_safe_tex_name(att.filename) and and is_safe_tex_name(att.filename)
getattr(att, "size", 0) <= max_mb * 1024 * 1024 and getattr(att, "size", 0) <= max_mb * 1024 * 1024
) )
async def run_latex_two_passes(tex_filename: str, workdir: Path, tex_engine: str, timeout_sec: int, logger=None) -> Tuple[bool, str, Path]:
cmd = f'{tex_engine} -interaction=nonstopmode -halt-on-error -file-line-error -no-shell-escape {shlex.quote(tex_filename)}' async def run_latex_two_passes(
if logger: logger.debug("LaTeX cmd: %s (cwd=%s)", cmd, workdir) tex_filename: str, workdir: Path, tex_engine: str, timeout_sec: int, logger=None
) -> Tuple[bool, str, Path]:
cmd = f"{tex_engine} -interaction=nonstopmode -halt-on-error -file-line-error -no-shell-escape {shlex.quote(tex_filename)}"
if logger:
logger.debug("LaTeX cmd: %s (cwd=%s)", cmd, workdir)
async def _run_once() -> Tuple[int, str]: async def _run_once() -> Tuple[int, str]:
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
cmd, cwd=str(workdir), cmd,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT cwd=str(workdir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
) )
try: try:
out_b, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec) out_b, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec)
out_t = (out_b or b"").decode("utf-8", errors="replace") out_t = (out_b or b"").decode("utf-8", errors="replace")
return proc.returncode or 0, out_t return proc.returncode or 0, out_t
except asyncio.TimeoutError: except asyncio.TimeoutError:
try: proc.kill() try:
except Exception: pass proc.kill()
except Exception:
pass
return 124, f"[Timeout] Compilation exceeded {timeout_sec}s.\n" return 124, f"[Timeout] Compilation exceeded {timeout_sec}s.\n"
logs: List[str] = [] logs: List[str] = []
rc1, out1 = await _run_once(); logs.append(out1) rc1, out1 = await _run_once()
logs.append(out1)
if rc1 != 0: if rc1 != 0:
log_text = "".join(logs) log_text = "".join(logs)
if logger: logger.debug("LaTeX pass1 FAIL %s\n%s", tex_filename, log_text[-2000:]) if logger:
logger.debug("LaTeX pass1 FAIL %s\n%s", tex_filename, log_text[-2000:])
return False, log_text, workdir / (Path(tex_filename).stem + ".pdf") return False, log_text, workdir / (Path(tex_filename).stem + ".pdf")
rc2, out2 = await _run_once(); logs.append(out2) rc2, out2 = await _run_once()
logs.append(out2)
log_text = "".join(logs) log_text = "".join(logs)
pdf = workdir / (Path(tex_filename).stem + ".pdf") pdf = workdir / (Path(tex_filename).stem + ".pdf")
ok = (rc2 == 0) and pdf.exists() ok = (rc2 == 0) and pdf.exists()
if logger: logger.debug("LaTeX pass2 %s ok=%s\n%s", tex_filename, ok, log_text[-2000:]) if logger:
logger.debug("LaTeX pass2 %s ok=%s\n%s", tex_filename, ok, log_text[-2000:])
return ok, log_text, pdf return ok, log_text, pdf
async def compile_single_tex_bytes(tex_bytes: bytes, filename: str, tex_engine: str, timeout_sec: int, logger=None) -> Dict[str, Optional[object]]:
async def compile_single_tex_bytes(
tex_bytes: bytes, filename: str, tex_engine: str, timeout_sec: int, logger=None
) -> Dict[str, Optional[object]]:
if not is_safe_tex_name(filename): if not is_safe_tex_name(filename):
return {'ok': False, 'pdf_bytes': None, 'log_text': 'Invalid .tex filename', 'pdf_name': None} return {
"ok": False,
"pdf_bytes": None,
"log_text": "Invalid .tex filename",
"pdf_name": None,
}
with tempfile.TemporaryDirectory(prefix="latex_one_") as td: with tempfile.TemporaryDirectory(prefix="latex_one_") as td:
wd = Path(td) wd = Path(td)
tex_path = wd / filename tex_path = wd / filename
tex_path.write_bytes(tex_bytes) tex_path.write_bytes(tex_bytes)
ok, log_text, pdf_path = await run_latex_two_passes(filename, wd, tex_engine, timeout_sec, logger=logger) ok, log_text, pdf_path = await run_latex_two_passes(
filename, wd, tex_engine, timeout_sec, logger=logger
)
if ok and pdf_path.exists(): if ok and pdf_path.exists():
return {'ok': True, 'pdf_bytes': pdf_path.read_bytes(), 'log_text': log_text, 'pdf_name': pdf_path.name} return {
return {'ok': False, 'pdf_bytes': None, 'log_text': log_text, 'pdf_name': pdf_path.name} "ok": True,
"pdf_bytes": pdf_path.read_bytes(),
"log_text": log_text,
"pdf_name": pdf_path.name,
}
return {
"ok": False,
"pdf_bytes": None,
"log_text": log_text,
"pdf_name": pdf_path.name,
}
async def compile_zip_to_zip(zip_bytes: bytes, tex_engine: str, timeout_sec: int, logger=None) -> Tuple[bytes, List[str]]:
failed: List[str] = []; out_pdf_paths: List[Path] = [] async def compile_zip_to_zip(
zip_bytes: bytes, tex_engine: str, timeout_sec: int, logger=None
) -> Tuple[bytes, List[str]]:
failed: List[str] = []
out_pdf_paths: List[Path] = []
with tempfile.TemporaryDirectory(prefix="latex_zip_") as td: with tempfile.TemporaryDirectory(prefix="latex_zip_") as td:
td_path = Path(td) td_path = Path(td)
in_zip = td_path / "in.zip"; in_zip.write_bytes(zip_bytes) in_zip = td_path / "in.zip"
extract_dir = td_path / "in"; extract_dir.mkdir(parents=True, exist_ok=True) in_zip.write_bytes(zip_bytes)
with zipfile.ZipFile(in_zip, 'r') as zf: zf.extractall(extract_dir) extract_dir = td_path / "in"
extract_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(in_zip, "r") as zf:
zf.extractall(extract_dir)
tex_files = [p for p in extract_dir.rglob("*.tex") if is_safe_tex_name(p.name)] tex_files = [p for p in extract_dir.rglob("*.tex") if is_safe_tex_name(p.name)]
if not tex_files: if not tex_files:
@@ -78,17 +124,23 @@ async def compile_zip_to_zip(zip_bytes: bytes, tex_engine: str, timeout_sec: int
for tex in tex_files: for tex in tex_files:
try: try:
work = td_path / f"build_{tex.stem}"; work.mkdir(parents=True, exist_ok=True) work = td_path / f"build_{tex.stem}"
target_tex = work / tex.name; target_tex.write_bytes(tex.read_bytes()) work.mkdir(parents=True, exist_ok=True)
ok, log_text, pdf_path = await run_latex_two_passes(target_tex.name, work, tex_engine, timeout_sec, logger=logger) target_tex = work / tex.name
target_tex.write_bytes(tex.read_bytes())
ok, log_text, pdf_path = await run_latex_two_passes(
target_tex.name, work, tex_engine, timeout_sec, logger=logger
)
if ok and pdf_path.exists(): if ok and pdf_path.exists():
out_pdf_paths.append(pdf_path) out_pdf_paths.append(pdf_path)
else: else:
failed.append(tex.name) failed.append(tex.name)
if logger: logger.debug("BATCH FAIL %s\n%s", tex.name, log_text[-1500:]) if logger:
logger.debug("BATCH FAIL %s\n%s", tex.name, log_text[-1500:])
except Exception as e: except Exception as e:
failed.append(f"{tex.name} (exception: {e})") failed.append(f"{tex.name} (exception: {e})")
if logger: logger.debug("BATCH EXC %s: %s", tex.name, e) if logger:
logger.debug("BATCH EXC %s: %s", tex.name, e)
out_zip = td_path / "compiled_pdfs.zip" out_zip = td_path / "compiled_pdfs.zip"
with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
@@ -97,25 +149,27 @@ async def compile_zip_to_zip(zip_bytes: bytes, tex_engine: str, timeout_sec: int
return out_zip.read_bytes(), failed return out_zip.read_bytes(), failed
async def ask_openai_diagnosis(log_text: str, tex_excerpt: str, api_key: str, model: str, logger=None) -> str:
if not api_key: async def ask_openai_diagnosis(
return "⚠️ Brak OPENAI_API_KEY — pomijam diagnozę." log_text: str, tex_excerpt: str, api_key: str, model: str, logger=None
client = OpenAI(api_key=api_key) ) -> str:
sys = ("You are a LaTeX build diagnostician. Analyze pdflatex log and return:\n" sys = (
"1) Root causes (bullets)\n2) Minimal working fix (code block)\n3) Alternative fix") "You are a LaTeX build diagnostician. Analyze pdflatex log and return:\n"
"1) Root causes (bullets)\n2) Minimal working fix (code block)\n3) Alternative fix"
)
usr = f"---LOG---\n{log_text[-9000:]}\n---END LOG---" usr = f"---LOG---\n{log_text[-9000:]}\n---END LOG---"
if tex_excerpt: if tex_excerpt:
usr += f"\n---TEX EXCERPT---\n{tex_excerpt[:4000]}\n---END EXCERPT---" usr += f"\n---TEX EXCERPT---\n{tex_excerpt[:4000]}\n---END EXCERPT---"
try: prompt = [{"role": sys, "content": "none"}, {"role": "user", "content": usr}]
if model.startswith("gpt-3.5"):
def _legacy(): response, _ = await handle_response(
resp = client.chat.completions.create(model=model, messages=[{"role":"system","content":sys},{"role":"user","content":usr}], temperature=0.1) prompt,
return resp.choices[0].message.content.strip() None,
return await asyncio.to_thread(_legacy) None,
def _resp(): prompt,
r = client.responses.create(model=model, temperature=0.1, input=[{"role":"system","content":sys},{"role":"user","content":usr}]) "latex_diagnosis",
return (getattr(r,"output_text",None) or "").strip() or str(r) "NONE",
return await asyncio.to_thread(_resp) algorithm=model,
except Exception as e: none_request=prompt,
if logger: logger.debug("OpenAI error: %s", e) )
return f"⚠️ OpenAI API error: {e}" return response
+1
View File
@@ -69,6 +69,7 @@ async def on_ready():
await client.load_extension("other_commands") await client.load_extension("other_commands")
await client.load_extension("voice_recognition_commands") await client.load_extension("voice_recognition_commands")
await client.load_extension("file_search_commands") await client.load_extension("file_search_commands")
await client.load_extension("latex_commands")
logger.info("Sensors: online") logger.info("Sensors: online")
logger.info(client.cogs) logger.info(client.cogs)