251 lines
10 KiB
Python
251 lines
10 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
import json
|
||
|
|
from typing import Any
|
||
|
|
import httpx
|
||
|
|
from app.config import settings
|
||
|
|
from app.db import execute, fetch_one
|
||
|
|
from app.services import ollama
|
||
|
|
from app.services import packaging_agent
|
||
|
|
|
||
|
|
AGENTS: dict[str, dict[str, str]] = {
|
||
|
|
"marketing": {"name": "Marketing", "persona": "Social, brand voice, campaigns for Foodlinkk."},
|
||
|
|
"bizdev": {"name": "BizDev", "persona": "Pipeline, retail partnerships, deal structuring."},
|
||
|
|
"finance": {"name": "Finance", "persona": "Margins, cashflow, pricing for food brands."},
|
||
|
|
"sourcing": {"name": "Sourcing", "persona": "Suppliers, MOQ, lead times, procurement."},
|
||
|
|
"product": {"name": "Product", "persona": "SKU development, launch timelines, shelf readiness."},
|
||
|
|
"halal": {"name": "Halal", "persona": "Halal compliance, certification, ingredient vetting."},
|
||
|
|
"design": {"name": "Design", "persona": "Packaging, visual identity, retail presentation."},
|
||
|
|
"packaging": {"name": "Packaging", "persona": "SVG/PDF verpakkingsontwerp, stanstekeningen, drukwerk."},
|
||
|
|
"knowledge": {"name": "Knowledge", "persona": "Internal docs, RAG, policy answers."},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
IMAGE_KEYWORDS = (
|
||
|
|
"maak foto", "maak een foto", "genereer foto", "genereer afbeelding",
|
||
|
|
"maak afbeelding", "productfoto", "genereer image", "generate image",
|
||
|
|
"make image", "maak plaatje", "/genfoto",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _wants_image(raw: str) -> bool:
|
||
|
|
t = (raw or "").strip().lower()
|
||
|
|
return any(k in t for k in IMAGE_KEYWORDS)
|
||
|
|
|
||
|
|
|
||
|
|
def _extract_image_prompt(raw: str) -> str:
|
||
|
|
t = raw.strip()
|
||
|
|
lower = t.lower()
|
||
|
|
for k in IMAGE_KEYWORDS:
|
||
|
|
if lower.startswith(k):
|
||
|
|
rest = t[len(k):].strip(" :,-")
|
||
|
|
if rest:
|
||
|
|
return rest
|
||
|
|
for k in IMAGE_KEYWORDS:
|
||
|
|
if k in lower:
|
||
|
|
idx = lower.index(k) + len(k)
|
||
|
|
rest = t[idx:].strip(" :,-")
|
||
|
|
if rest:
|
||
|
|
return rest
|
||
|
|
return t
|
||
|
|
|
||
|
|
|
||
|
|
async def _log_event(
|
||
|
|
agent_name: str,
|
||
|
|
event_type: str,
|
||
|
|
title: str,
|
||
|
|
body: str,
|
||
|
|
metadata: dict | None = None,
|
||
|
|
channel: str = "herman",
|
||
|
|
) -> None:
|
||
|
|
payload = {
|
||
|
|
"agent_name": agent_name,
|
||
|
|
"agent_type": "herman_delegate",
|
||
|
|
"event_type": event_type,
|
||
|
|
"title": title[:255],
|
||
|
|
"body": body,
|
||
|
|
"metadata": metadata or {},
|
||
|
|
"status": "completed",
|
||
|
|
"channel": channel,
|
||
|
|
}
|
||
|
|
try:
|
||
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
|
|
await client.post(f"{settings.TOOLS_API_URL.rstrip('/')}/events", json=payload)
|
||
|
|
except Exception:
|
||
|
|
try:
|
||
|
|
execute(
|
||
|
|
"""INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel, metadata)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb)""",
|
||
|
|
(agent_name, "herman_delegate", event_type, title[:255], body, "completed", "herman", json.dumps(metadata or {})),
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _pick_agent(raw: str) -> str:
|
||
|
|
text = (raw or "").strip().lower()
|
||
|
|
first = text.split()[0].replace(",", "").replace(".", "") if text else "knowledge"
|
||
|
|
if first in AGENTS:
|
||
|
|
return first
|
||
|
|
for k in AGENTS:
|
||
|
|
if k in text:
|
||
|
|
return k
|
||
|
|
return "knowledge"
|
||
|
|
|
||
|
|
def _agent_steps(delegated: list[str], routing_reason: str = "", extra: list[dict] | None = None) -> list[dict[str, str]]:
|
||
|
|
steps: list[dict[str, str]] = []
|
||
|
|
for a in delegated:
|
||
|
|
key = (a or "").strip().lower()
|
||
|
|
if key and key != "herman":
|
||
|
|
steps.append({"agent": key, "status": "delegated", "message": routing_reason or "Aangestuurd door Herman"})
|
||
|
|
if extra:
|
||
|
|
steps.extend(extra)
|
||
|
|
return steps
|
||
|
|
|
||
|
|
|
||
|
|
async def chat(message: str, channel: str = "cockpit") -> dict[str, Any]:
|
||
|
|
if packaging_agent.wants_packaging(message):
|
||
|
|
try:
|
||
|
|
outcome = await packaging_agent.generate_from_message(message)
|
||
|
|
name = outcome.get("design_name") or "Design"
|
||
|
|
pid = outcome.get("packaging_id", "")[:8]
|
||
|
|
reply_lines = [
|
||
|
|
f"Packaging agent heeft een design klaar voor je: {name}",
|
||
|
|
f"Type: {outcome.get('type')} · {outcome.get('dimensions')}",
|
||
|
|
f"Project #{outcome.get('cockpit_project_id')}",
|
||
|
|
f"Studio: {outcome.get('studio_url')}",
|
||
|
|
f"PDF: {outcome.get('pdf_url')}",
|
||
|
|
]
|
||
|
|
if outcome.get("nas", {}).get("ok"):
|
||
|
|
reply_lines.append("Bestanden staan op de NAS in de packaging-map van het project.")
|
||
|
|
reply = "\n".join(reply_lines)
|
||
|
|
|
||
|
|
await _log_event(
|
||
|
|
"packaging",
|
||
|
|
"packaging_created",
|
||
|
|
f"Design klaar: {name}",
|
||
|
|
f"Herman-opdracht: {message[:1500]}\n\nDesign-ID: {outcome.get('packaging_id')}",
|
||
|
|
{
|
||
|
|
"packaging_id": outcome.get("packaging_id"),
|
||
|
|
"project_id": outcome.get("cockpit_project_id"),
|
||
|
|
"studio_url": outcome.get("studio_url"),
|
||
|
|
"pdf_url": outcome.get("pdf_url"),
|
||
|
|
"nas": outcome.get("nas"),
|
||
|
|
"for_herman": True,
|
||
|
|
"delegated": ["packaging"],
|
||
|
|
"channel": channel,
|
||
|
|
},
|
||
|
|
channel=channel,
|
||
|
|
)
|
||
|
|
await _log_event(
|
||
|
|
"herman",
|
||
|
|
"packaging_delivered",
|
||
|
|
f"Packaging → Herman: {name}",
|
||
|
|
reply[:2000],
|
||
|
|
{
|
||
|
|
"source_agent": "packaging",
|
||
|
|
"packaging_id": outcome.get("packaging_id"),
|
||
|
|
"project_id": outcome.get("cockpit_project_id"),
|
||
|
|
"delegated": ["packaging"],
|
||
|
|
"channel": channel,
|
||
|
|
},
|
||
|
|
channel=channel,
|
||
|
|
)
|
||
|
|
delegated = ["packaging"]
|
||
|
|
return {
|
||
|
|
"agent": "packaging",
|
||
|
|
"agent_label": "Packaging → Herman",
|
||
|
|
"reply": reply,
|
||
|
|
"delegated_agents": delegated,
|
||
|
|
"routing_reason": "Packaging-opdracht gedetecteerd — design gegenereerd en aan Herman gerapporteerd",
|
||
|
|
"agent_steps": _agent_steps(delegated, "Packaging design gegenereerd"),
|
||
|
|
"packaging_id": outcome.get("packaging_id"),
|
||
|
|
"packaging_studio_url": outcome.get("studio_url"),
|
||
|
|
"packaging_pdf_url": outcome.get("pdf_url"),
|
||
|
|
}
|
||
|
|
except Exception as exc:
|
||
|
|
await _log_event(
|
||
|
|
"packaging",
|
||
|
|
"packaging_error",
|
||
|
|
"Packaging generatie mislukt",
|
||
|
|
str(exc)[:1500],
|
||
|
|
{"message": message[:500]},
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"agent": "packaging",
|
||
|
|
"agent_label": "Packaging",
|
||
|
|
"reply": f"Packaging agent kon geen design maken: {exc}",
|
||
|
|
"delegated_agents": ["packaging"],
|
||
|
|
}
|
||
|
|
|
||
|
|
if _wants_image(message):
|
||
|
|
prompt = _extract_image_prompt(message)
|
||
|
|
try:
|
||
|
|
async with httpx.AsyncClient(timeout=620.0) as client:
|
||
|
|
r = await client.post(
|
||
|
|
f"{settings.TOOLS_API_URL.rstrip('/')}/images/generate",
|
||
|
|
json={"prompt": prompt, "width": 512, "height": 512, "steps": 15},
|
||
|
|
)
|
||
|
|
r.raise_for_status()
|
||
|
|
data = r.json()
|
||
|
|
filename = data.get("filename", "")
|
||
|
|
subfolder = data.get("subfolder", "")
|
||
|
|
img_type = data.get("type", "output")
|
||
|
|
proxy = f"/api/ai/generated-image?filename={filename}&subfolder={subfolder}&type={img_type}"
|
||
|
|
reply = f"Afbeelding gegenereerd voor: {prompt}"
|
||
|
|
await _log_event("design", "image_generated", "ComfyUI via Herman", prompt[:500], {"filename": filename, "channel": channel}, channel=channel)
|
||
|
|
delegated = ["design"]
|
||
|
|
return {
|
||
|
|
"agent": "design",
|
||
|
|
"agent_label": "Design",
|
||
|
|
"reply": reply,
|
||
|
|
"image_url": proxy,
|
||
|
|
"prompt": prompt,
|
||
|
|
"delegated_agents": delegated,
|
||
|
|
"agent_steps": _agent_steps(delegated, "Afbeelding gegenereerd"),
|
||
|
|
}
|
||
|
|
except Exception as exc:
|
||
|
|
return {
|
||
|
|
"agent": "design",
|
||
|
|
"agent_label": "Design",
|
||
|
|
"reply": f"Kon geen afbeelding genereren: {exc}",
|
||
|
|
}
|
||
|
|
|
||
|
|
try:
|
||
|
|
async with httpx.AsyncClient(timeout=620.0) as client:
|
||
|
|
r = await client.post(
|
||
|
|
f"{settings.HERMAN_ORCHESTRATOR_URL.rstrip('/')}/chat",
|
||
|
|
json={"message": message, "agent": "default", "use_crm": True, "use_rag": True, "channel": channel},
|
||
|
|
)
|
||
|
|
r.raise_for_status()
|
||
|
|
data = r.json()
|
||
|
|
delegated = data.get("delegated_agents") or [data.get("agent", "herman")]
|
||
|
|
reason = data.get("routing_reason", "") or ""
|
||
|
|
await _log_event(
|
||
|
|
"herman",
|
||
|
|
"telegram_delegation" if channel == "telegram" else "openswarm_delegation",
|
||
|
|
f"Herman → {', '.join(delegated)}",
|
||
|
|
message[:2000],
|
||
|
|
{"delegated": delegated, "delegated_agents": delegated, "reason": reason, "channel": channel},
|
||
|
|
channel=channel,
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"agent": data.get("agent", "herman"),
|
||
|
|
"agent_label": data.get("agent_label", "Herman"),
|
||
|
|
"reply": data.get("reply", ""),
|
||
|
|
"delegated_agents": delegated,
|
||
|
|
"routing_reason": reason,
|
||
|
|
"agent_steps": _agent_steps(delegated, reason),
|
||
|
|
"rag_sources": data.get("rag_sources") or [],
|
||
|
|
"crm_loaded": data.get("crm_loaded", False),
|
||
|
|
}
|
||
|
|
except Exception as exc:
|
||
|
|
return {
|
||
|
|
"agent": "herman",
|
||
|
|
"agent_label": "Herman",
|
||
|
|
"reply": f"Herman orchestrator niet bereikbaar: {exc}",
|
||
|
|
}
|
||
|
|
|
||
|
|
async def generate_briefing() -> str:
|
||
|
|
from app.services.briefing import generate_daily_briefing
|
||
|
|
content, stats = await generate_daily_briefing()
|
||
|
|
await _log_event("herman", "briefing", "CEO briefing generated", content[:1500], {"stats": stats})
|
||
|
|
return content
|