Files

268 lines
8.1 KiB
Python
Raw Permalink Normal View History

"""Web Builder agent — websites bouwen via Hermes/agy API op VM107."""
from __future__ import annotations
import os
import re
from typing import Any
import httpx
from app.config import settings
from app.services.agent_events_log import log_agent_event
HERMES_BUILD_URL = os.getenv("HERMES_BUILD_URL", "http://10.4.7.27:8798").rstrip("/")
HERMES_PREVIEW_BASE = os.getenv("HERMES_PREVIEW_BASE", "http://10.4.7.27:8080").rstrip("/")
WEBSITE_KEYWORDS = (
"maak website",
"maak een website",
"bouw website",
"bouw een website",
"genereer website",
"website voor",
"website bouwen",
"landingspagina",
"landing page",
"webshop",
"webpagina",
"nieuwe site",
"nieuwe website",
"/website",
"web builder",
"webbuilder",
"agy",
"antigravity",
"anti gravity",
"anti-gravity",
"site maken",
"maak een site",
"bouw een site",
"website laten maken",
"laat agy",
"via agy",
)
PROJECT_RE = re.compile(
r"(?:website|site|project)\s+(?:voor\s+|called\s+|genaamd\s+)?['\"]?([a-z0-9][a-z0-9 _-]{1,48})['\"]?",
re.I,
)
FOR_RE = re.compile(
r"\bvoor\s+['\"]?([a-z0-9][a-z0-9 _-]{1,48})['\"]?",
re.I,
)
def wants_website(raw: str) -> bool:
t = (raw or "").strip().lower()
return any(k in t for k in WEBSITE_KEYWORDS)
def slugify(name: str) -> str:
s = (name or "").strip().lower()
s = re.sub(r"[^a-z0-9_-]+", "-", s)
s = re.sub(r"-+", "-", s).strip("-")
return s[:64] or "website"
def extract_project_name(raw: str) -> str:
t = (raw or "").strip()
m = PROJECT_RE.search(t)
if m:
return slugify(m.group(1))
m = FOR_RE.search(t)
if m:
name = m.group(1)
name = re.split(r"\s+(?:met|with|incl|including)\b", name, maxsplit=1, flags=re.I)[0]
return slugify(name)
for token in t.split():
clean = slugify(token)
if len(clean) >= 3 and clean not in (
"maak", "bouw", "website", "site", "voor", "een", "the", "and", "met",
"simpele", "landing", "page", "pagina", "landingspagina",
):
return clean
return slugify(t[:40]) or "website"
def extract_build_prompt(raw: str, project: str) -> str:
body = (raw or "").strip()
lower = body.lower()
for k in WEBSITE_KEYWORDS:
if lower.startswith(k):
rest = body[len(k) :].strip(" :,-")
if rest:
return rest
return (
f"Bouw een complete, moderne, responsive website voor project '{project}'. "
f"Opdracht: {body}. "
"Gebruik index.html, style.css, script.js en een assets/ map. "
"Foodlinkk halal kant-en-klaar food branding waar passend."
)
async def _hermes_get(path: str, timeout: float = 30.0) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.get(f"{HERMES_BUILD_URL}{path}")
try:
data = resp.json()
except Exception:
data = {"ok": False, "detail": resp.text[:500]}
if resp.status_code >= 400:
data.setdefault("ok", False)
return data
async def _hermes_post(path: str, payload: dict[str, Any], timeout: float = 60.0) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(f"{HERMES_BUILD_URL}{path}", json=payload)
try:
data = resp.json()
except Exception:
data = {"ok": False, "detail": resp.text[:500]}
if resp.status_code >= 400:
data.setdefault("ok", False)
data.setdefault("detail", resp.text[:500])
return data
async def list_projects() -> dict[str, Any]:
return await _hermes_get("/api/projects")
async def get_build_status(project: str) -> dict[str, Any]:
slug = slugify(project)
return await _hermes_get(f"/api/build/{slug}/status")
async def build_from_message(raw: str, *, channel: str = "herman") -> dict[str, Any]:
project = extract_project_name(raw)
prompt = extract_build_prompt(raw, project)
log_agent_event(
"webbuilder",
"website_build_start",
f"Start build: {project}",
prompt[:4000],
channel=channel,
metadata={"project": project, "prompt": prompt[:500]},
status="running",
)
data = await _hermes_post(
"/api/build",
{"project": project, "prompt": prompt},
timeout=45.0,
)
if not data.get("ok", True) and data.get("detail"):
log_agent_event(
"webbuilder",
"website_build_error",
f"Build mislukt: {project}",
str(data.get("detail", data))[:2000],
channel=channel,
metadata={"project": project},
status="error",
)
raise RuntimeError(str(data.get("detail") or data))
job_id = data.get("job_id") or project
preview = data.get("preview_url") or f"{HERMES_PREVIEW_BASE}/"
nas_path = data.get("nas_path") or f"//10.4.7.11/share/Websites/{project}/"
log_agent_event(
"webbuilder",
"website_build_running",
f"agy bezig: {project}",
f"Job {job_id}\nPreview: {preview}\nNAS: {nas_path}",
channel=channel,
metadata={
"project": project,
"job_id": job_id,
"preview_url": preview,
"nas_path": nas_path,
"correlation_id": job_id,
},
status="running",
)
return {
"project": project,
"job_id": job_id,
"preview_url": preview,
"nas_path": nas_path,
"prompt": prompt,
"hermes_status": data.get("status", "running"),
}
async def poll_until_done(project: str, *, channel: str = "herman", timeout: float = 1800.0) -> dict[str, Any]:
"""Poll Hermes build status until completed or failed."""
import asyncio
slug = slugify(project)
elapsed = 0.0
interval = 5.0
last_log = ""
while elapsed < timeout:
st = await get_build_status(slug)
status = str(st.get("status") or "unknown")
msg = str(st.get("message") or status)
if msg != last_log:
log_agent_event(
"webbuilder",
"website_build_progress",
msg[:255],
(st.get("log_tail") or "")[:4000],
channel=channel,
metadata={"project": slug, "status": status},
status="running" if status in ("running", "queued") else status,
)
last_log = msg
if status == "completed":
files = st.get("files") or []
preview = st.get("preview_url") or f"{HERMES_PREVIEW_BASE}/"
log_agent_event(
"webbuilder",
"website_build_done",
f"Website klaar: {slug}",
f"Bestanden: {', '.join(files[:8])}\nPreview: {preview}",
channel=channel,
metadata={
"project": slug,
"preview_url": preview,
"files": files,
"for_herman": True,
"source_agent": "webbuilder",
},
status="completed",
)
return {**st, "project": slug, "preview_url": preview}
if status == "failed":
detail = str(st.get("error") or st.get("message") or "Build mislukt")
log_agent_event(
"webbuilder",
"website_build_error",
f"Build mislukt: {slug}",
detail[:2000],
channel=channel,
metadata={"project": slug},
status="error",
)
raise RuntimeError(detail)
await asyncio.sleep(interval)
elapsed += interval
raise TimeoutError(f"Build timeout voor {slug} na {int(timeout)}s")
async def generate_from_message(raw: str, *, channel: str = "herman", wait: bool = True) -> dict[str, Any]:
started = await build_from_message(raw, channel=channel)
if wait:
finished = await poll_until_done(started["project"], channel=channel)
started.update(finished)
return started