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(")", "\\)")