mirror of
https://github.com/migatu/conjurer.git
synced 2026-07-14 13:34:40 +00:00
154 lines
4.3 KiB
Python
154 lines
4.3 KiB
Python
# latex_functions.py
|
|
# -*- coding: utf-8 -*-
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import re
|
|
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
|
|
|
|
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_asset_name(name: str) -> bool:
|
|
return bool(SAFE_ASSET_NAME.match(name or ""))
|
|
|
|
|
|
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
|
|
)
|
|
|
|
|
|
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,
|
|
]
|
|
|
|
|
|
async def run_latex_two_passes(
|
|
tex_filename: str,
|
|
workdir: Path,
|
|
tex_engine: str,
|
|
timeout_sec: int,
|
|
logger: str = None,
|
|
):
|
|
logger = logging.getLogger(logger) if logger else 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:
|
|
try:
|
|
proc.kill()
|
|
except Exception:
|
|
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)
|
|
|
|
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
|