SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user