From 28685780f39a8cb24bf7166693bb1c7d0153d4ea Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Wed, 20 Aug 2025 21:59:11 +0200 Subject: [PATCH] Vibe coding :P --- latex_commands.py | 254 +++++++++++++++++++++++++++++++---------- latex_functions.py | 273 ++++++++++++++++++++++++--------------------- 2 files changed, 344 insertions(+), 183 deletions(-) diff --git a/latex_commands.py b/latex_commands.py index 25a9303..d25cdcc 100644 --- a/latex_commands.py +++ b/latex_commands.py @@ -1,22 +1,47 @@ # latex_commands.py # -*- coding: utf-8 -*- from __future__ import annotations + import io -from typing import Optional, List import logging +from typing import Optional import discord from discord import app_commands from discord.ext import commands from constants import ( - LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, LATEX_MAX_ATTACH_MB, LATEX_MAX_ZIP_MB, - OPENAI_MODEL, ALLOWED_ROLES, GUILD_ID + ALLOWED_ROLES, + GUILD_ID, + LATEX_MAX_ATTACH_MB, + LATEX_MAX_COMPILE_SECONDS, + LATEX_MAX_ZIP_MB, + LATEX_TEX_ENGINE, + OPENAI_MODEL, ) from latex_functions import ( - is_tex_attachment, compile_single_tex_bytes, compile_zip_to_zip, ask_openai_diagnosis + compile_single_tex_bytes, + compile_zip_to_zip, + is_safe_asset_name, + is_safe_tex_name, ) + +async def _collect_attachments_bytes(attachments, max_mb: int): + """ + Zwraca słownik {filename: bytes} dla wszystkich załączników, do limitu MB. + Nie podbija wyjątków z .read() — zostawiamy to wyżej; tu zwracamy, co się udało. + """ + out = {} + for a in attachments or []: + # limit rozmiaru + if (getattr(a, "size", 0) or 0) > max_mb * 1024 * 1024: + continue + data = await a.read() + out[a.filename] = data + return out + + class LatexModule(commands.Cog): def __init__(self, bot: commands.Bot, logger_name: str): self.bot = bot @@ -31,47 +56,94 @@ class LatexModule(commands.Cog): guild=None if GUILD_ID is None else discord.Object(id=GUILD_ID), ) @commands.has_any_role(*ALLOWED_ROLES) - async def tex(self, ctx: commands.Context, include_source_excerpt: Optional[bool] = True): + async def tex( + self, ctx: commands.Context, include_source_excerpt: Optional[bool] = True + ): self.logger.info("LaTeX command invoked by %s", ctx.author) ch = ctx.message.channel + # ZBIERZ WSZYSTKIE ZAŁĄCZNIKI async with ch.typing(): - atts = [a for a in (ctx.message.attachments or []) if is_tex_attachment(a, LATEX_MAX_ATTACH_MB)] - if not atts: - return await ctx.reply(f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False) - self.logger.debug("LaTeX attachments: %s", ", ".join(a.filename for a in atts)) - files: List[discord.File] = [] - parts: List[str] = [] - for att in atts: + all_bytes = await _collect_attachments_bytes( + ctx.message.attachments, LATEX_MAX_ATTACH_MB + ) + if not all_bytes: + return await ctx.reply( + f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False + ) + + # PODZIEL NA GŁÓWNE .TEX i WSPIERAJĄCE + tex_names = [ + n + for n in all_bytes.keys() + if n.lower().endswith(".tex") and is_safe_tex_name(n) + ] + if not tex_names: + return await ctx.reply( + f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False + ) + + support_names = [ + n + for n in all_bytes.keys() + if n not in tex_names and is_safe_asset_name(n) + ] + + files_to_send = [] + parts = [] + + for tex_name in tex_names: try: - data = await att.read() - self.logger.info("LaTeX read %s bytes from %s", len(data), att.filename) - res = await compile_single_tex_bytes(data, att.filename, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name) - self.logger.info("LaTeX result for %s: %s", att.filename, res) - self.logger.info("LaTeX log (%s)\n%s", att.filename, (res["log_text"] or "")[-2000:]) + main_bytes = all_bytes[tex_name] + support = [ + (n, all_bytes[n]) + for n in support_names + if n != tex_name and is_safe_asset_name(n) + ] + # (ważne) do supportów dopuszczamy również **inne .tex** – dla \input/\include + other_tex = [ + (n, all_bytes[n]) + for n in tex_names + if n != tex_name and is_safe_tex_name(n) + ] + support.extend(other_tex) + + res = await compile_single_tex_bytes( + main_bytes, + tex_name, + LATEX_TEX_ENGINE, + LATEX_MAX_COMPILE_SECONDS, + logger="discord", + support_files=support, + ) + + # Twoja dotychczasowa obsługa success/fail – przykładowo: if res["ok"] and res["pdf_bytes"]: b = io.BytesIO(res["pdf_bytes"]) b.seek(0) b.name = res["pdf_name"] - files.append(discord.File(b, filename=res["pdf_name"])) - parts.append(f"✅ `{att.filename}` → `{res['pdf_name']}`") + files_to_send.append(discord.File(b, filename=res["pdf_name"])) + parts.append(f"✅ `{tex_name}` → `{res['pdf_name']}`") else: - self.logger.info("LaTeX compile failed for %s", att.filename) - excerpt = "" - if include_source_excerpt: - try: - excerpt = data.decode("utf-8", errors="ignore")[:4000] - except Exception: - excerpt = "" - advice = await ask_openai_diagnosis(res["log_text"] or "", excerpt, OPENAI_MODEL, logger=self.logger_name) - parts.append(f"❌ `{att.filename}` — błąd kompilacji.\nDiagnoza:\n{advice}") - raise - except Exception as e: - self.logger.info("Exception %s: %s", att.filename, e) - parts.append(f"⚠️ `{att.filename}` — wyjątek (szczegóły w logu).") + parts.append( + f"❌ `{tex_name}` — błąd kompilacji. (log: { (res['log_text'] or '')[-400] })" + ) + # Tutaj wywołujesz swoją ścieżkę do OpenAI diagnozy, którą ogarnęliśmy wcześniej. + except Exception: + # let it crash — pełny stack w logu i re-raise (Discord.py to pokaże sensownie) + logging.getLogger("discord").exception( + "Unhandled exception while compiling %s", tex_name + ) raise - content = f"**LaTeX** — {len(atts)} plik(ów). Model: `{OPENAI_MODEL}`\n\n" + "\n\n".join(parts) - await ctx.reply(content if len(content)<1900 else content[:1900]+"…", files=files, mention_author=False) + content = ( + f"**LaTeX** — {len(tex_names)} plik(ów). Model: `{OPENAI_MODEL}`\n\n" + + "\n\n".join(parts) + ) + await ctx.reply( + content if len(content) < 1900 else content[:1900] + "…", + files=parts, + mention_author=False, + ) # -------- /latexclean (PDF only; log -> debug logger) -------- @commands.hybrid_command( @@ -84,57 +156,123 @@ class LatexModule(commands.Cog): async def latexclean(self, ctx: commands.Context): ch = ctx.message.channel async with ch.typing(): - atts = [a for a in (ctx.message.attachments or []) if is_tex_attachment(a, LATEX_MAX_ATTACH_MB)] - if not atts: - return await ctx.reply(f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False) + all_bytes = await _collect_attachments_bytes( + ctx.message.attachments, LATEX_MAX_ATTACH_MB + ) + if not all_bytes: + return await ctx.reply( + f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False + ) - files: List[discord.File] = [] - parts: List[str] = [] - for att in atts: + # PODZIEL NA GŁÓWNE .TEX i WSPIERAJĄCE + tex_names = [ + n + for n in all_bytes.keys() + if n.lower().endswith(".tex") and is_safe_tex_name(n) + ] + if not tex_names: + return await ctx.reply( + f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False + ) + + support_names = [ + n + for n in all_bytes.keys() + if n not in tex_names and is_safe_asset_name(n) + ] + + files_to_send = [] + parts = [] + + for tex_name in tex_names: try: - data = await att.read() - res = await compile_single_tex_bytes(data, att.filename, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name) - self.logger.debug("LaTeX CLEAN (%s)\n%s", att.filename, (res["log_text"] or "")[-2000:]) + main_bytes = all_bytes[tex_name] + support = [ + (n, all_bytes[n]) + for n in support_names + if n != tex_name and is_safe_asset_name(n) + ] + # (ważne) do supportów dopuszczamy również **inne .tex** – dla \input/\include + other_tex = [ + (n, all_bytes[n]) + for n in tex_names + if n != tex_name and is_safe_tex_name(n) + ] + support.extend(other_tex) + + res = await compile_single_tex_bytes( + main_bytes, + tex_name, + LATEX_TEX_ENGINE, + LATEX_MAX_COMPILE_SECONDS, + logger="discord", + support_files=support, + ) + + # Twoja dotychczasowa obsługa success/fail – przykładowo: if res["ok"] and res["pdf_bytes"]: b = io.BytesIO(res["pdf_bytes"]) b.seek(0) b.name = res["pdf_name"] - files.append(discord.File(b, filename=res["pdf_name"])) - parts.append(f"✅ `{att.filename}`") + files_to_send.append(discord.File(b, filename=res["pdf_name"])) + parts.append(f"✅ `{tex_name}` → `{res['pdf_name']}`") else: - parts.append(f"❌ `{att.filename}` — błąd (log w debug).") - except Exception as e: - self.logger.debug("CLEAN exception %s: %s", att.filename, e) - parts.append(f"⚠️ `{att.filename}` — wyjątek (log w debug).") + parts.append( + f"❌ `{tex_name}` — błąd kompilacji. (log: { (res['log_text'] or '')[-400] })" + ) + # Tutaj wywołujesz swoją ścieżkę do OpenAI diagnozy, którą ogarnęliśmy wcześniej. + except Exception: + # let it crash — pełny stack w logu i re-raise (Discord.py to pokaże sensownie) + logging.getLogger("discord").exception( + "Unhandled exception while compiling %s", tex_name + ) raise - - await ctx.reply("**LaTeX clean** — " + ", ".join(parts), files=files, mention_author=False) + await ctx.reply( + "**LaTeX clean** — " + ", ".join(parts), + files=tex_names, + mention_author=False, + ) # -------- /texbatch (ZIP -> ZIP) -------- @app_commands.command( name="texbatch", - description="Wyślij ZIP z .tex → odeślę ZIP z PDF-ami (tylko udane)." + description="Wyślij ZIP z .tex → odeślę ZIP z PDF-ami (tylko udane).", ) @app_commands.describe(archive=f"Archiwum .zip (max ~{LATEX_MAX_ZIP_MB} MiB)") @app_commands.checks.has_any_role(*ALLOWED_ROLES) - async def texbatch(self, interaction: discord.Interaction, archive: discord.Attachment): + async def texbatch( + self, interaction: discord.Interaction, archive: discord.Attachment + ): await interaction.response.defer() if not archive or not archive.filename.lower().endswith(".zip"): - return await interaction.followup.send("Dołącz ZIP (.zip) z plikami .tex.", ephemeral=True) + return await interaction.followup.send( + "Dołącz ZIP (.zip) z plikami .tex.", ephemeral=True + ) if archive.size > LATEX_MAX_ZIP_MB * 1024 * 1024: - return await interaction.followup.send(f"ZIP > {LATEX_MAX_ZIP_MB} MiB — za duży.", ephemeral=True) + return await interaction.followup.send( + f"ZIP > {LATEX_MAX_ZIP_MB} MiB — za duży.", ephemeral=True + ) data = await archive.read() - out_zip_bytes, failed = await compile_zip_to_zip(data, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name) + out_zip_bytes, failed = await compile_zip_to_zip( + data, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name + ) if not out_zip_bytes and failed: - return await interaction.followup.send("Brak PDF-ów. " + "; ".join(failed[:10]), ephemeral=True) + return await interaction.followup.send( + "Brak PDF-ów. " + "; ".join(failed[:10]), ephemeral=True + ) bio = io.BytesIO(out_zip_bytes) bio.seek(0) bio.name = "compiled_pdfs.zip" if 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): logger = logging.getLogger("discord") diff --git a/latex_functions.py b/latex_functions.py index a3d7bd1..e03d5c1 100644 --- a/latex_functions.py +++ b/latex_functions.py @@ -1,153 +1,176 @@ -# latex_functions.py -# -*- coding: utf-8 -*- -from __future__ import annotations - import asyncio import logging import re +import shutil import tempfile import zipfile from pathlib import Path -from typing import Dict, List, Optional, Tuple - -from ai_functions import handle_response -import shutil -from typing import Iterable, Tuple +from typing import Dict, Iterable, List, Optional, Tuple +# ===== Bezpieczeństwo nazw/rozszerzeń ===== +SAFE_TEX_NAME = re.compile(r"^[\w\-. ]+\.tex$", re.IGNORECASE) SAFE_ASSET_NAME = re.compile( r"^[\w\-. /]+?\.(tex|png|jpg|jpeg|pdf|svg|eps|bmp|gif|sty|cls|bib|bst|bbx|cbx|def|cfg)$", re.IGNORECASE, ) +def is_safe_tex_name(name: str) -> bool: + return bool(SAFE_TEX_NAME.match(name or "")) + def is_safe_asset_name(name: str) -> bool: return bool(SAFE_ASSET_NAME.match(name or "")) +# ===== Uruchamianie kompilatora ===== -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 +async def _run(cmd: List[str], cwd: Path, timeout_sec: int, logger_name: Optional[str]) -> Tuple[int, str]: + """Uruchamia proces kompilatora w cwd, zwraca (rc, sklejone_stdout_stderr).""" + logger = logging.getLogger(logger_name) if logger_name else logging.getLogger() + logger.debug("RUN %s (cwd=%s, timeout=%s)", " ".join(cmd), cwd, timeout_sec) + + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=str(cwd), ) + try: + out = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec) + except asyncio.TimeoutError: + try: + proc.kill() + finally: + pass + return 124, "TIMEOUT: compiler took too long" + rc = proc.returncode + log = (out[0] or b"").decode("utf-8", errors="ignore") + logger.debug("RC=%s, LOG TAIL:\n%s", rc, log[-1500:]) + return rc, log - -def _build_cmd(engine: str, tex_filename: str) -> list[str]: - base = (engine or "").strip().lower() - if base in ("pdflatex", "xelatex", "lualatex"): - return [ - base, - "-interaction=nonstopmode", - "-halt-on-error", - "-file-line-error", - "-no-shell-escape", - tex_filename, - ] - if base == "latexmk": - return [ - "latexmk", - "-pdf", - "-interaction=nonstopmode", - "-halt-on-error", - "-file-line-error", - tex_filename, - ] - if base == "tectonic": - return [ - "tectonic", - "-X", - "compile", - "--keep-logs", - "--outdir", - ".", - tex_filename, - ] - return [ - "pdflatex", - "-interaction=nonstopmode", - "-halt-on-error", - "-file-line-error", - "-no-shell-escape", - tex_filename, - ] - +def _build_cmd(tex_engine: str, main_tex: str) -> List[str]: + """Buduje komendę kompilatora; domyślnie tectonic/pdflatex/xelatex.""" + if tex_engine.lower() == "tectonic": + # Tectonic sam robi wielofazowość, ale i tak uruchamiamy 2x (bezpiecznie dla bib/refs) + return ["tectonic", "--keep-intermediates", "--synctex", main_tex] + elif tex_engine.lower() in ("pdflatex", "xelatex", "lualatex"): + return [tex_engine, "-interaction=nonstopmode", "-halt-on-error", main_tex] + else: + # fallback – traktujemy jak pdflatex + return ["pdflatex", "-interaction=nonstopmode", "-halt-on-error", main_tex] async def run_latex_two_passes( - tex_filename: str, - workdir: Path, + tex_filename: str, workdir: Path, tex_engine: str, timeout_sec: int, logger: Optional[str] = None +) -> Tuple[bool, str, Path]: + """ + Uruchamia kompilację 2 razy (bezpieczeństwo referencji/bib). + Zwraca: (ok, log_text, pdf_path). + """ + logger_obj = logging.getLogger(logger) if logger else logging.getLogger() + logger_obj.info("Compiling %s with %s engine", tex_filename, tex_engine) + logs: List[str] = [] + pdf_path = workdir / (Path(tex_filename).stem + ".pdf") + + # PASS 1 + rc1, log1 = await _run(_build_cmd(tex_engine, tex_filename), workdir, timeout_sec, logger) + logs.append(log1) + if rc1 != 0: + return False, "".join(logs), pdf_path + + # PASS 2 (często już nic nie robi, ale nie szkodzi) + rc2, log2 = await _run(_build_cmd(tex_engine, tex_filename), workdir, timeout_sec, logger) + logs.append(log2) + ok = (rc2 == 0) and pdf_path.exists() + return ok, "".join(logs), pdf_path + +# ===== Kompilacje: pojedynczy TEX + ZIP ===== + +async def compile_single_tex_bytes( + tex_bytes: bytes, + filename: str, tex_engine: str, timeout_sec: int, - logger: str = None, -): - logger = logging.getLogger(logger) if logger else None + logger: Optional[str] = None, + support_files: Optional[Iterable[Tuple[str, bytes]]] = None, +) -> Dict[str, Optional[object]]: + """ + Kompiluje pojedynczy .tex + dowolne pliki pomocnicze (obrazy, sty/cls/bib, inne .tex). + Każdy plik zapisujemy w katalogu roboczym widocznym dla kompilatora. + """ + log = logging.getLogger(logger) if logger else logging.getLogger() + log.info("Compiling %s with %s engine", filename, tex_engine) + if not is_safe_tex_name(filename): + return {"ok": False, "pdf_bytes": None, "log_text": "Invalid .tex filename", "pdf_name": None} - async def run_once(cmd: list[str]) -> tuple[int, str]: - proc = await asyncio.create_subprocess_exec( - *cmd, - cwd=str(workdir), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - ) - try: - out_b, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec) - return (proc.returncode or 0), (out_b or b"").decode( - "utf-8", errors="replace" - ) - except asyncio.TimeoutError: + with tempfile.TemporaryDirectory(prefix="latex_one_") as td: + wd = Path(td) + (wd / Path(filename).name).write_bytes(tex_bytes) + + # Zapisz wszystkie supporty obok głównego pliku + if support_files: + for fname, data in support_files: + if not fname or not is_safe_asset_name(fname): + continue + # spłaszczamy do nazwy bazowej (bez podkatalogów z zewn. źródeł) + (wd / Path(fname).name).write_bytes(data) + + ok, log_text, pdf_path = await run_latex_two_passes(Path(filename).name, wd, tex_engine, timeout_sec, 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} + +async def compile_zip_to_zip( + zip_bytes: bytes, tex_engine: str, timeout_sec: int, logger: Optional[str] = None +) -> Tuple[bytes, List[str]]: + """ + Rozpakowuje ZIP, znajduję wszystkie .tex i dla KAŻDEGO tworzy osobny katalog roboczy + ze skopiowanym CAŁYM drzewem, by zachować ścieżki względne obrazków/plików. + Zwraca (zip_pdf_bytes, lista_rel_sciezek_które_padły). + """ + log = logging.getLogger(logger) if logger else logging.getLogger() + failed: List[str] = [] + out_pdf_paths: List[Path] = [] + + with tempfile.TemporaryDirectory(prefix="latex_zip_") as td: + td_path = Path(td) + src_root = td_path / "src" + src_root.mkdir(parents=True, exist_ok=True) + + # 1) Rozpakuj + zpath = td_path / "in.zip" + zpath.write_bytes(zip_bytes) + with zipfile.ZipFile(zpath, "r") as zf: + zf.extractall(src_root) + + # 2) Zbierz .tex + tex_files = [p for p in src_root.rglob("*.tex") if is_safe_tex_name(p.name)] + if not tex_files: + return b"", ["No .tex files found in archive."] + + # 3) Kompiluj każdy .tex w izolacji (kopiujemy całe drzewo do osobnego workdiru) + for tex_path in tex_files: + rel_tex = tex_path.relative_to(src_root).as_posix() + work = td_path / f"build_{tex_path.stem}" try: - proc.kill() + shutil.copytree(src_root, work / "src") + ok, log_text, pdf = await run_latex_two_passes(rel_tex, work / "src", tex_engine, timeout_sec, logger) + if ok and pdf.exists(): + out_pdf_paths.append(pdf) + else: + failed.append(rel_tex) + log.info("LaTeX FAIL %s\n%s", rel_tex, (log_text or "")[-2000:]) except Exception: + # let it crash – log + re-raise (to zobaczy wyższa warstwa/discord.py) + log.exception("Exception while compiling %s", rel_tex) raise - return 124, f"[Timeout] Compilation exceeded {timeout_sec}s.\n" - logs = [] - cmd = _build_cmd(tex_engine, tex_filename) - if logger: - logger.debug("LaTeX cmd: %s (cwd=%s)", " ".join(cmd), workdir) + # 4) Spakuj wyjściowe PDF-y + out_zip = td_path / "compiled_pdfs.zip" + with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for pdf in out_pdf_paths: + zf.write(pdf, arcname=pdf.name) - rc1, out1 = await run_once(cmd) - logs.append(out1) - second_pass = not ((tex_engine or "").strip().lower() == "tectonic") - - if rc1 != 0: - log_text = "".join(logs) - pdf = workdir / (Path(tex_filename).stem + ".pdf") - return False, log_text, pdf - - if second_pass: - rc2, out2 = await run_once(cmd) - logs.append(out2) - ok = rc2 == 0 - else: - ok = rc1 == 0 - - log_text = "".join(logs) - pdf = workdir / (Path(tex_filename).stem + ".pdf") - ok = ok and pdf.exists() - return ok, log_text, pdf - -async def ask_openai_diagnosis( - log_text: str, tex_excerpt: str, model: str, logger: str = None -) -> str: - logger = logging.getLogger(logger) if logger else None - 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" - ) - logger.debug("LaTeX DIAG (%s)\n%s", model, log_text[-9000:]) - 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---" - 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 + return out_zip.read_bytes(), failed