Files
foodlinkk-command-center/cockpit/app/services/packaging_agent.py
T

233 lines
7.3 KiB
Python
Raw Normal View History

2026-06-09 10:41:13 +00:00
"""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,
}