Files
foodlinkk-command-center/cockpit/app/services/packaging_agent.py
T
2026-06-09 10:41:13 +00:00

233 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Packaging agent — parse Herman-opdrachten en genereer designs."""
from __future__ import annotations
import re
from typing import Any
import httpx
from app.config import settings
from app.services import packaging_nas, projects
PACKAGING_KEYWORDS = (
"maak verpakking",
"maak een verpakking",
"genereer verpakking",
"ontwerp verpakking",
"packaging:",
"packaging ",
"/packaging",
"maak packaging",
"maak package",
"stanstekening",
"verpakkingsontwerp",
)
TYPE_ALIASES: list[tuple[str, str]] = [
("folding box", "folding_box"),
("folding_box", "folding_box"),
("sluitdoos", "folding_box"),
("doos", "folding_box"),
("banderole", "wrap"),
("wrap", "wrap"),
("rond label", "round_label"),
("round label", "round_label"),
("round_label", "round_label"),
("sleeve", "sleeve"),
("huls", "sleeve"),
("pouch", "pouch"),
("zak", "pouch"),
("tray", "tray"),
("schaal", "tray"),
]
DEFAULT_ELEMENTS = {
"barcode": True,
"logo_area": True,
"fold_lines": True,
"cut_lines": True,
"nutrition_panel": False,
"ingredients": True,
"halal_badge": False,
"window": False,
"qr_code": False,
"glue_tabs": False,
"bleed": True,
"dimensions": True,
}
def wants_packaging(raw: str) -> bool:
t = (raw or "").strip().lower()
return any(k in t for k in PACKAGING_KEYWORDS)
def extract_packaging_body(raw: str) -> str:
t = raw.strip()
lower = t.lower()
for k in PACKAGING_KEYWORDS:
if lower.startswith(k):
rest = t[len(k) :].strip(" :,-")
if rest:
return rest
for k in PACKAGING_KEYWORDS:
if k in lower:
idx = lower.index(k) + len(k)
rest = t[idx:].strip(" :,-")
if rest:
return rest
return t
def _detect_type(text: str) -> str:
lower = text.lower()
for alias, ptype in TYPE_ALIASES:
if alias in lower:
return ptype
return "folding_box"
def _detect_dimensions(text: str) -> tuple[float, float, float]:
m = re.search(r"(\d{2,4})\s*[x×]\s*(\d{2,4})(?:\s*[x×]\s*(\d{1,4}))?", text, re.I)
if m:
w, h = float(m.group(1)), float(m.group(2))
d = float(m.group(3)) if m.group(3) else (40.0 if _detect_type(text) == "folding_box" else 20.0)
return w, h, d
return 120.0, 80.0, 40.0
def _detect_project_id(text: str) -> int | None:
m = re.search(r"project\s*#?\s*(\d+)", text, re.I)
if m:
return int(m.group(1))
m = re.search(r"\bproject\s+(\d+)\b", text, re.I)
return int(m.group(1)) if m else None
def _detect_product_name(text: str) -> str:
m = re.search(r'voor\s+["\']?([^"\']+?)["\']?(?:\s+project|\s*$|,)', text, re.I)
if m:
return m.group(1).strip()[:80]
m = re.search(r'product\s*[:=]\s*["\']?([^"\']+)["\']?', text, re.I)
if m:
return m.group(1).strip()[:80]
cleaned = text
for alias, _ in TYPE_ALIASES:
cleaned = re.sub(re.escape(alias), "", cleaned, flags=re.I)
cleaned = re.sub(r"\d{2,4}\s*[x×]\s*\d{2,4}(?:\s*[x×]\s*\d{1,4})?", "", cleaned, flags=re.I)
cleaned = re.sub(r"project\s*#?\s*\d+", "", cleaned, flags=re.I)
for kw in ("halal-badge", "halal badge", "voedingswaarden", "nutrition", "qr-code", "qr code", "venster", "window"):
cleaned = re.sub(re.escape(kw), "", cleaned, flags=re.I)
cleaned = cleaned.strip(" ,:-")
return (cleaned[:80] or "Foodlinkk Product")
def parse_packaging_request(message: str) -> dict[str, Any]:
body = extract_packaging_body(message)
lower = body.lower()
ptype = _detect_type(body)
w, h, d = _detect_dimensions(body)
product = _detect_product_name(body)
project_id = _detect_project_id(message) or _detect_project_id(body)
elements = dict(DEFAULT_ELEMENTS)
if any(k in lower for k in ("halal", "halal-badge", "halal badge")):
elements["halal_badge"] = True
if any(k in lower for k in ("voedingswaarden", "nutrition")):
elements["nutrition_panel"] = True
if any(k in lower for k in ("qr", "qrcode")):
elements["qr_code"] = True
if "venster" in lower or "window" in lower:
elements["window"] = True
return {
"type": ptype,
"width_mm": w,
"height_mm": h,
"depth_mm": d,
"bleed_mm": 3,
"design_name": product,
"barcode_value": "8710000000012",
"elements": elements,
"text": {
"product_name": product,
"tagline": "Premium halal kant-en-klaar",
"subtitle": "",
"ingredients": "",
"origin": "Geproduceerd in NL",
"best_before": "Ten minste houdbaar tot: zie verpakking",
},
"brand": {},
"project_id": project_id,
"created_by": "packaging",
}
async def _download_bytes(packaging_id: str, fmt: str) -> bytes | None:
try:
async with httpx.AsyncClient(timeout=120.0) as client:
r = await client.get(
f"{settings.TOOLS_API_URL.rstrip('/')}/packaging/download/{packaging_id}",
params={"format": fmt},
)
if r.status_code < 400:
return r.content
except Exception:
pass
return None
async def generate_from_message(message: str) -> dict[str, Any]:
"""Genereer packaging design en exporteer naar NAS; retour voor Herman-reply."""
spec = parse_packaging_request(message)
if not spec.get("project_id"):
proj = projects.create_project(
f"Packaging · {spec['design_name'][:48]}",
description=f"Aangemaakt door packaging agent via Herman\n\nOpdracht: {message[:500]}",
project_type="packaging",
ensure_nas=True,
created_by="packaging",
)
spec["project_id"] = proj.get("id")
async with httpx.AsyncClient(timeout=120.0) as client:
r = await client.post(
f"{settings.TOOLS_API_URL.rstrip('/')}/packaging/generate",
json=spec,
)
r.raise_for_status()
result = r.json()
packaging_id = result.get("id", "")
cockpit_project_id = result.get("cockpit_project_id") or spec.get("project_id")
svg = result.get("svg") or ""
saved_spec = result.get("spec") or spec
nas_info: dict[str, Any] = {}
if packaging_id and cockpit_project_id and svg:
png_b = await _download_bytes(packaging_id, "png")
pdf_b = await _download_bytes(packaging_id, "pdf")
try:
nas_info = packaging_nas.export_packaging_files(
packaging_id, int(cockpit_project_id), svg, saved_spec, png_b, pdf_b
)
except Exception as exc:
nas_info = {"ok": False, "error": str(exc)}
studio_url = f"/packaging?project_id={cockpit_project_id}"
pdf_url = f"/api/packaging/download/{packaging_id}?format=pdf"
return {
"packaging_id": packaging_id,
"cockpit_project_id": cockpit_project_id,
"design_name": saved_spec.get("design_name") or spec.get("design_name"),
"type": saved_spec.get("type"),
"dimensions": f"{saved_spec.get('width_mm')}×{saved_spec.get('height_mm')}×{saved_spec.get('depth_mm')} mm",
"studio_url": studio_url,
"pdf_url": pdf_url,
"nas": nas_info,
"spec": saved_spec,
"original_message": message,
}