Bot & Automation

AI Assistants

/root/hermes-projects/AI Assistants

apps/whatsapp_control_api/app/services/file_delivery.py text
from __future__ import annotations

import re
import textwrap
import zlib
from dataclasses import dataclass
from pathlib import Path


FILE_DELIVERABLE_KEYWORDS = (
    "pdf",
    "dokumen",
    "document",
    "laporan",
    "proposal",
    "storyboard",
    "file",
    "ebook",
    "whitepaper",
    "panduan",
)

PDF_KEYWORDS = (
    "pdf",
    "format pdf",
    "file pdf",
    "dokumen pdf",
    "export pdf",
    "ekspor pdf",
    "simpan sebagai pdf",
)

FILE_REFUSAL_PATTERNS = (
    "saya tidak bisa langsung mengirimkan file",
    "saya tidak bisa membuat file",
    "saya tidak bisa membuat pdf",
    "tidak bisa langsung membuat file",
    "tidak bisa langsung mengirim file",
    "tidak bisa langsung mengirimkan file",
    "copy-paste",
    "copy paste",
    "salin ke microsoft word",
    "salin ke word",
    "salin ke google docs",
    "lalu simpan sebagai pdf",
    "lalu ekspor sebagai pdf",
)


@dataclass(frozen=True)
class FileDeliveryResult:
    summary: str
    created_files: tuple[Path, ...]


def is_file_deliverable_request(prompt: str) -> bool:
    lowered = prompt.lower()
    return any(keyword in lowered for keyword in FILE_DELIVERABLE_KEYWORDS)


def is_pdf_request(prompt: str) -> bool:
    lowered = prompt.lower()
    return any(keyword in lowered for keyword in PDF_KEYWORDS)


def summary_refuses_file_delivery(summary: str) -> bool:
    lowered = summary.lower()
    return any(pattern in lowered for pattern in FILE_REFUSAL_PATTERNS)


def has_existing_file_path(summary: str, projects_root: Path, suffixes: set[str] | None = None) -> bool:
    root = projects_root.resolve()
    for candidate in _extract_project_paths(summary, root):
        path = candidate.resolve()
        if not _is_path_within(path, root):
            continue
        if not path.exists() or not path.is_file():
            continue
        if suffixes and path.suffix.lower() not in suffixes:
            continue
        return True
    return False


def ensure_pdf_delivery(task_id: str, prompt: str, summary: str, projects_root: Path) -> FileDeliveryResult:
    if not is_pdf_request(prompt):
        return FileDeliveryResult(summary=summary, created_files=())
    if has_existing_file_path(summary, projects_root, {".pdf"}):
        return FileDeliveryResult(summary=summary, created_files=())

    output_dir = projects_root / "AI Assistants Outputs" / task_id
    output_dir.mkdir(parents=True, exist_ok=True)
    title = _derive_title(prompt)
    body = _clean_document_body(summary, prompt)

    markdown_path = output_dir / f"{_slugify(title)}-{task_id}.md"
    pdf_path = output_dir / f"{_slugify(title)}-{task_id}.pdf"

    markdown_path.write_text(_build_markdown_document(title, prompt, body), encoding="utf-8")
    write_simple_pdf(pdf_path, title, body)

    final_summary = _append_file_summary(summary, (pdf_path, markdown_path), replaced_refusal=summary_refuses_file_delivery(summary))
    return FileDeliveryResult(summary=final_summary, created_files=(pdf_path, markdown_path))


def write_simple_pdf(path: Path, title: str, body: str) -> None:
    lines = _wrap_pdf_lines(title, body)
    width = 595
    height = 842
    margin_x = 54
    start_y = 792
    line_height = 14
    lines_per_page = max(1, int((start_y - 54) / line_height))
    pages = [lines[index : index + lines_per_page] for index in range(0, len(lines), lines_per_page)] or [[""]]

    objects: list[bytes] = [b"<< /Type /Catalog /Pages 2 0 R >>"]
    objects.append(b"")
    page_object_ids: list[int] = []

    for page_index, page_lines in enumerate(pages):
        content_id = 3 + (page_index * 2)
        page_id = content_id + 1
        page_object_ids.append(page_id)
        content = _build_page_stream(page_lines, margin_x, start_y, line_height)
        compressed = zlib.compress(content.encode("latin-1", errors="replace"))
        objects.append(b"<< /Length %d /Filter /FlateDecode >>\nstream\n" % len(compressed) + compressed + b"\nendstream")
        objects.append(
            (
                "<< /Type /Page /Parent 2 0 R "
                f"/MediaBox [0 0 {width} {height}] "
                f"/Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> "
                f"/F2 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >> >> >> "
                f"/Contents {content_id} 0 R >>"
            ).encode("ascii")
        )

    kids = " ".join(f"{page_id} 0 R" for page_id in page_object_ids)
    objects[1] = f"<< /Type /Pages /Kids [{kids}] /Count {len(page_object_ids)} >>".encode("ascii")

    output = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = [0]
    for index, obj in enumerate(objects, start=1):
        offsets.append(len(output))
        output.extend(f"{index} 0 obj\n".encode("ascii"))
        output.extend(obj)
        output.extend(b"\nendobj\n")
    xref_offset = len(output)
    output.extend(f"xref\n0 {len(objects) + 1}\n".encode("ascii"))
    output.extend(b"0000000000 65535 f \n")
    for offset in offsets[1:]:
        output.extend(f"{offset:010d} 00000 n \n".encode("ascii"))
    output.extend(
        (
            f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\n"
            f"startxref\n{xref_offset}\n%%EOF\n"
        ).encode("ascii")
    )
    path.write_bytes(bytes(output))


def _extract_project_paths(text: str, projects_root: Path) -> list[Path]:
    root_pattern = re.escape(projects_root.as_posix())
    fallback_pattern = re.escape("/root/hermes-projects")
    matches = re.findall(rf"({root_pattern}/[^\n`]+|{fallback_pattern}/[^\n`]+)", text)
    return [Path(match.strip().strip(" .,:;)")) for match in matches]


def _is_path_within(path: Path, parent: Path) -> bool:
    try:
        path.resolve().relative_to(parent.resolve())
        return True
    except ValueError:
        return False


def _derive_title(prompt: str) -> str:
    cleaned = re.sub(r"\s+", " ", prompt).strip(" .")
    cleaned = re.sub(r"^(buatkan|buat|tolong buatkan|silakan buatkan)\s+(saya\s+)?", "", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b(dokumen|file|pdf|profesional|tentang)\b", "", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" .-/")
    if not cleaned:
        return "Dokumen Hermes"
    return cleaned[:90].strip()


def _slugify(value: str) -> str:
    normalized = re.sub(r"[^a-zA-Z0-9]+", "-", value.lower()).strip("-")
    return normalized[:48] or "dokumen"


def _clean_document_body(summary: str, prompt: str) -> str:
    text = summary.strip()
    if not text:
        return prompt.strip()

    refusal_regexes = (
        r"(?is)^karena\s+saya.*?(?:berikut\s+(?:naskah|struktur|dokumen|storyboard)(?:nya)?\s*[:.-]\s*)",
        r"(?is)^saya\s+.*?tidak\s+bisa.*?(?:berikut\s+(?:naskah|struktur|dokumen|storyboard)(?:nya)?\s*[:.-]\s*)",
    )
    for pattern in refusal_regexes:
        text = re.sub(pattern, "", text).strip()
    return text or summary.strip() or prompt.strip()


def _build_markdown_document(title: str, prompt: str, body: str) -> str:
    return "\n\n".join(
        [
            f"# {title}",
            "## Permintaan",
            prompt.strip(),
            "## Isi Dokumen",
            body.strip(),
        ]
    ).strip() + "\n"


def _append_file_summary(summary: str, paths: tuple[Path, ...], *, replaced_refusal: bool) -> str:
    base = _clean_document_body(summary, "") if replaced_refusal else summary.strip()
    lines = [base.strip()] if base.strip() else ["File sudah dibuat."]
    lines.extend(["", "File hasil:"])
    for path in paths:
        lines.append(f"- {path}")
    return "\n".join(lines).strip()


def _wrap_pdf_lines(title: str, body: str) -> list[tuple[str, bool]]:
    lines: list[tuple[str, bool]] = []
    lines.append((_to_pdf_text(title), True))
    lines.append(("", False))
    for raw_line in body.splitlines():
        stripped = raw_line.strip()
        if not stripped:
            lines.append(("", False))
            continue
        is_heading = stripped.startswith("#")
        cleaned = re.sub(r"^#{1,6}\s*", "", stripped)
        cleaned = re.sub(r"\*\*(.*?)\*\*", r"\1", cleaned)
        cleaned = re.sub(r"\*(.*?)\*", r"\1", cleaned)
        prefix = "- " if stripped.startswith(("-", "*")) else ""
        cleaned = prefix + cleaned.lstrip("-* ").strip()
        wrapped = textwrap.wrap(_to_pdf_text(cleaned), width=86) or [""]
        for wrapped_line in wrapped:
            lines.append((wrapped_line, is_heading))
    return lines


def _to_pdf_text(value: str) -> str:
    replacements = {
        "\u2013": "-",
        "\u2014": "-",
        "\u2018": "'",
        "\u2019": "'",
        "\u201c": '"',
        "\u201d": '"',
        "\u2022": "-",
        "\u00a0": " ",
    }
    for old, new in replacements.items():
        value = value.replace(old, new)
    return value.encode("latin-1", errors="replace").decode("latin-1")


def _build_page_stream(lines: list[tuple[str, bool]], margin_x: int, start_y: int, line_height: int) -> str:
    commands = ["BT"]
    y = start_y
    for text, bold in lines:
        font = "F2" if bold else "F1"
        size = 14 if bold else 10
        escaped = _escape_pdf_string(text)
        commands.append(f"/{font} {size} Tf")
        commands.append(f"1 0 0 1 {margin_x} {y} Tm")
        commands.append(f"({escaped}) Tj")
        y -= line_height + (4 if bold else 0)
    commands.append("ET")
    return "\n".join(commands)


def _escape_pdf_string(value: str) -> str:
    return value.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")