From f0bd6c4015cc18e2bce3f5be8146f0abc1c063b5 Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Sat, 16 Aug 2025 17:54:06 +0200 Subject: [PATCH] Latex incoming changes --- ai_functions.py | 123 ++++++++++++++++++----------------- latex_commands.py | 5 +- latex_functions.py | 158 ++++++++++++++++++++++++++++++--------------- thin_client.py | 1 + 4 files changed, 174 insertions(+), 113 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index 9c6cbc4..633f2ba 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -133,7 +133,7 @@ def num_tokens_from_string(message, model): 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 @@ -189,60 +189,63 @@ async def handle_response( # convert back to json. json.dump(file_data, file_memory, indent=4) history = [] - history.append(GPT_SETTINGS[0]) - chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4") + if request_type != "NONE": + 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(): - if not reakcja[3]: - content = ( - "Kiedy słyszysz " - + slowo - + " to reagujesz lub dzieje się to " - + reakcja[0] - ) - temp = {"role": "system", "content": content} - chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") - history.append(temp) + for slowo, reakcja in WORD_REACTIONS.items(): + if not reakcja[3]: + content = ( + "Kiedy słyszysz " + + slowo + + " to reagujesz lub dzieje się to " + + reakcja[0] + ) + temp = {"role": "system", "content": content} + chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") + history.append(temp) - 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") + final_prompt = username + ":" + prompt logger.debug( - "Rozmiar zapytania %s prompt %s temp %s", - chat_gpt_config_request_size, - prompt_gpt_request_size, - temp_token, + "Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size ) - 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) + 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( + "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) try: # ...przygotowanie messages/system prompt/itp. jak masz... @@ -256,9 +259,9 @@ async def handle_response( except openai.APITimeoutError as e: # 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: - 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: # Handle invalid request error, e.g. validate parameters or log resp, _ = await handle_response( @@ -269,7 +272,7 @@ async def handle_response( username, "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: # Handle invalid request error, e.g. validate parameters or log resp, _ = await handle_response( @@ -280,20 +283,20 @@ async def handle_response( username, "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: # 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: # 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: - 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: - 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: # 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(history) diff --git a/latex_commands.py b/latex_commands.py index c878758..6dfd731 100644 --- a/latex_commands.py +++ b/latex_commands.py @@ -3,6 +3,7 @@ from __future__ import annotations import io from typing import Optional, List +import logging import discord from discord import app_commands @@ -128,5 +129,7 @@ class LatexModule(commands.Cog): 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)]) -async def setup(bot: commands.Bot): +async def setup(bot): + logger = logging.getLogger("discord") await bot.add_cog(LatexModule(bot, logger_name="latex")) + logger.info("Loading kinky latex module done") diff --git a/latex_functions.py b/latex_functions.py index 5d00655..f16bfdd 100644 --- a/latex_functions.py +++ b/latex_functions.py @@ -1,76 +1,122 @@ # latex_functions.py # -*- coding: utf-8 -*- 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) + def is_safe_tex_name(name: str) -> bool: return bool(SAFE_TEX_NAME.match(name or "")) and name.lower().endswith(".tex") + def is_tex_attachment(att, max_mb: int) -> bool: """Filtruje discord.Attachment: tylko .tex + limit MB + nazwa bezpieczna.""" return bool( - getattr(att, "filename", "") and - is_safe_tex_name(att.filename) and - getattr(att, "size", 0) <= max_mb * 1024 * 1024 + getattr(att, "filename", "") + and is_safe_tex_name(att.filename) + 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)}' - if logger: logger.debug("LaTeX cmd: %s (cwd=%s)", cmd, workdir) + +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)}" + if logger: + logger.debug("LaTeX cmd: %s (cwd=%s)", cmd, workdir) async def _run_once() -> Tuple[int, str]: proc = await asyncio.create_subprocess_shell( - cmd, cwd=str(workdir), - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT + cmd, + cwd=str(workdir), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, ) try: out_b, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec) out_t = (out_b or b"").decode("utf-8", errors="replace") return proc.returncode or 0, out_t except asyncio.TimeoutError: - try: proc.kill() - except Exception: pass + try: + proc.kill() + except Exception: + pass return 124, f"[Timeout] Compilation exceeded {timeout_sec}s.\n" logs: List[str] = [] - rc1, out1 = await _run_once(); logs.append(out1) + rc1, out1 = await _run_once() + logs.append(out1) if rc1 != 0: 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") - rc2, out2 = await _run_once(); logs.append(out2) + rc2, out2 = await _run_once() + logs.append(out2) log_text = "".join(logs) pdf = workdir / (Path(tex_filename).stem + ".pdf") 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 -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): - 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: wd = Path(td) tex_path = wd / filename 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(): - return {'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} + return { + "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: td_path = Path(td) - in_zip = td_path / "in.zip"; in_zip.write_bytes(zip_bytes) - 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) + in_zip = td_path / "in.zip" + in_zip.write_bytes(zip_bytes) + 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)] 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: try: - work = td_path / f"build_{tex.stem}"; work.mkdir(parents=True, exist_ok=True) - 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) + work = td_path / f"build_{tex.stem}" + work.mkdir(parents=True, exist_ok=True) + 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(): out_pdf_paths.append(pdf_path) else: 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: 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" 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 -async def ask_openai_diagnosis(log_text: str, tex_excerpt: str, api_key: str, model: str, logger=None) -> str: - if not api_key: - return "⚠️ Brak OPENAI_API_KEY — pomijam diagnozę." - client = OpenAI(api_key=api_key) - sys = ("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") + +async def ask_openai_diagnosis( + log_text: str, tex_excerpt: str, api_key: str, model: str, logger=None +) -> str: + sys = ( + "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---" if tex_excerpt: usr += f"\n---TEX EXCERPT---\n{tex_excerpt[:4000]}\n---END EXCERPT---" - try: - if model.startswith("gpt-3.5"): - def _legacy(): - resp = client.chat.completions.create(model=model, messages=[{"role":"system","content":sys},{"role":"user","content":usr}], temperature=0.1) - return resp.choices[0].message.content.strip() - return await asyncio.to_thread(_legacy) - def _resp(): - r = client.responses.create(model=model, temperature=0.1, input=[{"role":"system","content":sys},{"role":"user","content":usr}]) - return (getattr(r,"output_text",None) or "").strip() or str(r) - return await asyncio.to_thread(_resp) - except Exception as e: - if logger: logger.debug("OpenAI error: %s", e) - return f"⚠️ OpenAI API error: {e}" + prompt = [{"role": sys, "content": "none"}, {"role": "user", "content": usr}] + + response, _ = await handle_response( + prompt, + None, + None, + prompt, + "latex_diagnosis", + "NONE", + algorithm=model, + none_request=prompt, + ) + return response diff --git a/thin_client.py b/thin_client.py index 38c4fa5..93b4a35 100755 --- a/thin_client.py +++ b/thin_client.py @@ -69,6 +69,7 @@ async def on_ready(): await client.load_extension("other_commands") await client.load_extension("voice_recognition_commands") await client.load_extension("file_search_commands") + await client.load_extension("latex_commands") logger.info("Sensors: online") logger.info(client.cogs)