SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC

This commit is contained in:
sysops
2026-06-23 10:04:23 +00:00
parent 3bf15c4850
commit 26fe76afdd
165 changed files with 47427 additions and 1264 deletions
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Matrix-driven agent scheduler — daily handoff chains with correlation_id."""
from __future__ import annotations
import json
import sys
import urllib.request
import uuid
from datetime import datetime, timezone
COCKPIT_URL = "http://127.0.0.1:8600"
TOOLS_URL = "http://127.0.0.1:8700"
def _post(url: str, payload: dict | None = None, timeout: int = 120) -> dict:
data = json.dumps(payload or {}).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def _handoff(from_agent: str, to_agent: str, handoff_type: str, payload: dict, cid: str) -> None:
try:
_post(
f"{COCKPIT_URL}/api/agents/handoff",
{
"from_agent": from_agent,
"to_agent": to_agent,
"handoff_type": handoff_type,
"payload": payload,
"correlation_id": cid,
},
)
print(f"handoff {from_agent} -> {to_agent} ({handoff_type})")
except Exception as exc:
print(f"handoff failed {from_agent}->{to_agent}: {exc}", file=sys.stderr)
def _trigger_tools(path: str, label: str) -> dict:
try:
return _post(f"{TOOLS_URL}{path}", timeout=180)
except Exception as exc:
print(f"{label} failed: {exc}", file=sys.stderr)
return {"ok": False, "error": str(exc)}
def run_chain_0600(cid: str) -> None:
research = _trigger_tools("/research/run", "research")
_handoff("browser", "research", "scrape", {"step": "morning_research"}, cid)
_handoff("research", "marketing", "intel", {"research_ok": research.get("ok", False)}, cid)
rss = _trigger_tools("/retail/rss/refresh", "rss")
_handoff("marketing", "retail", "trends", {"rss_ok": rss.get("ok", True), "items": rss.get("count", 0)}, cid)
def run_chain_0630(cid: str) -> None:
_handoff("sourcing", "product", "suppliers", {"step": "morning_sourcing"}, cid)
_handoff("product", "halal", "compliance", {"step": "ingredient_check"}, cid)
def run_chain_0700(cid: str) -> None:
_handoff("sysops", "knowledge", "logs", {"step": "infra_archive"}, cid)
_handoff("hr", "sysops", "onboarding", {"step": "hr_check", "note": "placeholder check-in"}, cid)
_handoff("sysops", "herman", "infra_status", {"step": "daily_ops"}, cid)
def run_chain_0800(cid: str) -> None:
_handoff("email", "bizdev", "leads", {"step": "morning_leads_digest"}, cid)
_handoff("retail", "bizdev", "opportunities", {"step": "top_opportunities"}, cid)
_handoff("bizdev", "finance", "valuation", {"step": "pipeline_margin"}, cid)
def main() -> int:
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
base_cid = str(uuid.uuid4())
print(f"agent_scheduler start {today} correlation={base_cid}")
run_chain_0600(base_cid)
run_chain_0630(str(uuid.uuid4()))
run_chain_0700(str(uuid.uuid4()))
run_chain_0800(str(uuid.uuid4()))
try:
_post(f"{COCKPIT_URL}/api/herman/briefing", timeout=300)
print("briefing triggered")
except Exception as exc:
print(f"briefing skip: {exc}", file=sys.stderr)
print("agent_scheduler done")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Remount Synology share with permissive options + restart doc-ingest
set -euo pipefail
PASS="${NAS_PASS:-Foodlinkk#2026}"
FSTAB_ENTRY='//10.4.7.11/share /mnt/synology cifs credentials=/etc/smbcredentials/synology,vers=3.0,uid=1000,gid=1000,file_mode=0775,dir_mode=0775,_netdev 0 0'
fix_host() {
local host="$1"
echo "==> Fix NAS mount on $host"
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no "aissa@$host" "PASS='$PASS' FSTAB_ENTRY='$FSTAB_ENTRY' bash -s" <<'REMOTE'
set -e
echo "$PASS" | sudo -S mkdir -p /etc/smbcredentials /mnt/synology
if [ ! -f /etc/smbcredentials/synology ]; then
echo "$PASS" | sudo -S bash -c "printf '%s\n' 'username=aissa' 'password=$PASS' > /etc/smbcredentials/synology"
echo "$PASS" | sudo -S chmod 600 /etc/smbcredentials/synology
fi
echo "$PASS" | sudo -S sed -i '/10\.4\.7\.11\/share/d' /etc/fstab
echo "$PASS" | sudo -S bash -c "echo '$FSTAB_ENTRY' >> /etc/fstab"
echo "$PASS" | sudo -S umount /mnt/synology 2>/dev/null || true
echo "$PASS" | sudo -S mount /mnt/synology
echo "Share files: $(find /mnt/synology -type f ! -path '*#recycle*' 2>/dev/null | wc -l)"
echo "CUCINA files: $(find /mnt/synology/CUCINA -type f 2>/dev/null | wc -l)"
REMOTE
}
fix_host "10.4.7.19"
fix_host "10.4.7.18"
echo "==> Restart doc-ingest"
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no aissa@10.4.7.19 \
'cd ~/foodlinkk-ai/doc-ingest && docker compose restart 2>/dev/null || docker restart foodlinkk-doc-ingest'
echo "==> Done"
echo "Als CUCINA/Foodlinkk nog 0 bestanden tonen: Synology DSM -> share -> Permissions -> aissa Read/Write + submappen"
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Hermes Web Builder API — agy builds voor Cockpit Web Builder agent."""
from __future__ import annotations
import asyncio
import json
import os
import re
import subprocess
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Hermes Web Builder API", version="1.0.0")
PROJECTS_BASE = Path(os.getenv("WEBBUILDER_PROJECTS", "/home/hermes/projects"))
PREVIEW_PORT = int(os.getenv("WEBBUILDER_PREVIEW_PORT", "8080"))
PREVIEW_HOST = os.getenv("WEBBUILDER_PREVIEW_HOST", "10.4.7.27")
NAS_PREFIX = os.getenv("WEBBUILDER_NAS_PREFIX", "//10.4.7.11/share/Websites")
AGY_BIN = os.getenv("AGY_BIN", "/home/hermes/.local/bin/agy")
BUILD_TIMEOUT = os.getenv("WEBBUILDER_BUILD_TIMEOUT", "30m")
JOBS: dict[str, dict[str, Any]] = {}
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 _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _list_files(project_dir: Path) -> list[str]:
if not project_dir.is_dir():
return []
out: list[str] = []
for p in sorted(project_dir.rglob("*")):
if p.is_file():
out.append(str(p.relative_to(project_dir)))
return out[:50]
def _preview_url() -> str:
return f"http://{PREVIEW_HOST}:{PREVIEW_PORT}/"
async def _run_shell(cmd: str, *, timeout: float = 120.0) -> tuple[int, str, str]:
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
return 124, "", "timeout"
return proc.returncode or 0, stdout.decode(errors="replace"), stderr.decode(errors="replace")
async def _ensure_project(slug: str) -> Path:
proj = PROJECTS_BASE / slug
proj.mkdir(parents=True, exist_ok=True)
await _run_shell(f"chown hermes:hermes '{proj}'")
return proj
async def _start_preview(slug: str) -> None:
cmd = (
f"pkill -f 'http.server.*{PREVIEW_PORT}' 2>/dev/null || true; "
f"su - hermes -c 'cd {PROJECTS_BASE}/{slug} && "
f"nohup python3 -m http.server {PREVIEW_PORT} > /tmp/webbuilder-preview.log 2>&1 &'"
)
await _run_shell(cmd, timeout=15.0)
async def _run_build(job_id: str, slug: str, prompt: str) -> None:
job = JOBS[job_id]
job["status"] = "running"
job["message"] = f"agy bouwt {slug}"
job["updated_at"] = _now()
proj = await _ensure_project(slug)
safe_prompt = prompt.replace('"', '\\"')[:8000]
agy_cmd = (
f'su - hermes -c \'export PATH="$HOME/.local/bin:$PATH"; '
f"cd '{proj}' && "
f"{AGY_BIN} --print --dangerously-skip-permissions --print-timeout {BUILD_TIMEOUT} "
f'"{safe_prompt}"\''
)
job["message"] = "Antigravity CLI gestart…"
rc, out, err = await _run_shell(agy_cmd, timeout=3600.0)
log_tail = (out + "\n" + err).strip()[-4000:]
job["log_tail"] = log_tail
job["updated_at"] = _now()
files = _list_files(proj)
job["files"] = files
if rc == 0 and files:
await _start_preview(slug)
job["status"] = "completed"
job["message"] = f"Website klaar — {len(files)} bestand(en)"
job["preview_url"] = _preview_url()
job["completed_at"] = _now()
elif rc == 0:
job["status"] = "completed"
job["message"] = "agy klaar (geen bestanden gevonden)"
job["preview_url"] = _preview_url() if files else None
job["completed_at"] = _now()
else:
job["status"] = "failed"
job["message"] = f"agy exit code {rc}"
job["error"] = log_tail[-800:] or f"exit {rc}"
job["completed_at"] = _now()
class BuildRequest(BaseModel):
project: str = Field(..., min_length=1, max_length=64)
prompt: str = Field(..., min_length=1, max_length=8000)
@app.get("/health")
async def health() -> dict[str, Any]:
return {"ok": True, "service": "hermes-webbuilder", "projects_base": str(PROJECTS_BASE)}
@app.get("/api/projects")
async def list_projects() -> dict[str, Any]:
items: list[dict[str, Any]] = []
if PROJECTS_BASE.is_dir():
for p in sorted(PROJECTS_BASE.iterdir()):
if p.is_dir() and not p.name.startswith("."):
files = _list_files(p)
items.append({"slug": p.name, "files": len(files), "has_index": (p / "index.html").is_file()})
return {"ok": True, "items": items, "count": len(items)}
@app.post("/api/build")
async def start_build(body: BuildRequest) -> dict[str, Any]:
slug = slugify(body.project)
if not slug:
raise HTTPException(400, "Ongeldige projectnaam")
await _ensure_project(slug)
job_id = str(uuid.uuid4())
JOBS[job_id] = {
"job_id": job_id,
"project": slug,
"status": "queued",
"message": "Build in wachtrij",
"prompt": body.prompt[:500],
"nas_path": f"{NAS_PREFIX}/{slug}/",
"preview_url": _preview_url(),
"created_at": _now(),
"updated_at": _now(),
}
JOBS[slug] = JOBS[job_id]
asyncio.create_task(_run_build(job_id, slug, body.prompt))
return {
"ok": True,
"job_id": job_id,
"project": slug,
"status": "queued",
"nas_path": f"{NAS_PREFIX}/{slug}/",
"preview_url": _preview_url(),
}
@app.get("/api/build/{project}/status")
async def build_status(project: str) -> dict[str, Any]:
slug = slugify(project)
job = JOBS.get(slug) or JOBS.get(project)
if not job:
proj = PROJECTS_BASE / slug
files = _list_files(proj)
if files:
return {
"ok": True,
"project": slug,
"status": "completed",
"message": "Project bestaat (geen actieve job)",
"files": files,
"preview_url": _preview_url(),
"nas_path": f"{NAS_PREFIX}/{slug}/",
}
return {"ok": True, "project": slug, "status": "idle", "message": "Geen build actief", "files": []}
return {
"ok": True,
"project": slug,
"job_id": job.get("job_id"),
"status": job.get("status"),
"message": job.get("message"),
"files": job.get("files") or [],
"preview_url": job.get("preview_url"),
"nas_path": f"{NAS_PREFIX}/{slug}/",
"log_tail": job.get("log_tail", ""),
"error": job.get("error"),
"updated_at": job.get("updated_at"),
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "8798")))
+237
View File
@@ -0,0 +1,237 @@
"""
Hermes Telegram bot — tekst + voice relay naar Cockpit Herman/Voice API.
Deploy naar VM105: ~/foodlinkk-ai/hermes/main.py
"""
from __future__ import annotations
import io
import logging
import os
from pathlib import Path
import httpx
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
COCKPIT_CHAT_URL = os.getenv("COCKPIT_HERMAN_URL", "http://10.4.7.18:8600/api/herman/chat")
COCKPIT_VOICE_TURN_URL = os.getenv("COCKPIT_VOICE_TURN_URL", "http://10.4.7.18:8600/api/voice/turn")
CHAT_IDS_FILE = Path(os.getenv("CHAT_IDS_FILE", os.path.expanduser("~/foodlinkk-ai/hermes/chat_ids.txt")))
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("hermes")
_chat_sessions: dict[int, str] = {}
def read_chat_ids() -> list[int]:
if not CHAT_IDS_FILE.is_file():
return []
return [int(line.strip()) for line in CHAT_IDS_FILE.read_text().splitlines() if line.strip().isdigit()]
def save_chat_id(chat_id: int) -> None:
if chat_id in set(read_chat_ids()):
return
CHAT_IDS_FILE.parent.mkdir(parents=True, exist_ok=True)
with CHAT_IDS_FILE.open("a") as f:
f.write(f"{chat_id}\n")
def _session_id(chat_id: int) -> str:
if chat_id not in _chat_sessions:
_chat_sessions[chat_id] = f"tg-{chat_id}-{os.getpid()}"
return _chat_sessions[chat_id]
def _format_delegation(data: dict) -> str:
agents = data.get("delegated_agents") or []
agents = [a for a in agents if str(a).lower() != "herman"]
if not agents:
return ""
reason = (data.get("routing_reason") or "").strip()
lines = ["", "👥 Agents: " + "".join(agents)]
steps = data.get("agent_steps") or []
for step in steps[:6]:
msg = step.get("message") or step.get("status") or ""
if msg:
lines.append(f" · {step.get('agent', '?')}: {msg[:120]}")
if not steps and reason:
lines.append(f"{reason[:200]}")
return "\n".join(lines)
def _format_ui_actions(data: dict) -> str:
herman = data.get("herman") if "herman" in data else data
actions = herman.get("ui_actions") or []
lines: list[str] = []
for action in actions:
if action.get("type") == "show_export_results":
items = action.get("entities") or []
total = action.get("total") or len(items)
lines.append(f"\n📦 Export Intel: {total} resultaten")
for ent in items[:12]:
name = ent.get("name") or "?"
city = ent.get("city") or ""
etype = ent.get("entity_type") or ""
contact = ent.get("primary_email") or ent.get("primary_phone") or ""
line = f" · {name}"
if city:
line += f" ({city})"
if etype:
line += f" [{etype}]"
if contact:
line += f"{contact[:40]}"
lines.append(line)
if total > 12:
lines.append(f" … en {total - 12} meer")
url = action.get("open_url") or "/export-intel"
lines.append(f" 🔗 http://10.4.7.18:8600{url}")
elif action.get("type") == "open_webbuilder_build":
project = action.get("project") or "website"
preview = action.get("preview_url") or ""
lines.append(f"\n🌐 Agy website build: {project}")
if preview:
lines.append(f" Preview: {preview}")
lines.append(" 🔗 http://10.4.7.18:8600/agents")
return "\n".join(lines)
def _format_pending(data: dict) -> str:
herman = data.get("herman") if "herman" in data else data
if not herman.get("needs_confirmation"):
return ""
pa = herman.get("pending_action") or {}
if not pa:
return ""
return (
"\n\n⚠️ Bevestiging: antwoord **ja** om te starten of **nee** om te annuleren."
+ (
f"\n(Agy website: {pa.get('project', '')})"
if pa.get("type") == "webbuilder_build"
else (f"\n({pa.get('entity_label', '')} in {pa.get('location_label', '')})" if pa.get("location_label") else "")
)
)
async def _call_herman(message: str, chat_id: int) -> dict:
async with httpx.AsyncClient(timeout=300.0) as client:
resp = await client.post(
COCKPIT_CHAT_URL,
json={
"message": message,
"channel": "telegram",
"session_id": _session_id(chat_id),
},
)
resp.raise_for_status()
data = resp.json()
return data if isinstance(data, dict) else {}
async def _call_voice_turn(audio_bytes: bytes, filename: str, mime: str, chat_id: int) -> dict:
async with httpx.AsyncClient(timeout=300.0) as client:
resp = await client.post(
COCKPIT_VOICE_TURN_URL,
data={"session_id": _session_id(chat_id)},
files={"file": (filename, audio_bytes, mime)},
)
resp.raise_for_status()
return resp.json()
async def _reply_herman_result(update: Update, status_msg, data: dict) -> None:
herman = data.get("herman") if data.get("herman") is not None else data
reply = (herman.get("reply") or data.get("reply") or "Geen antwoord.").strip()
if data.get("text") and not reply.startswith(str(data["text"])[:20]):
reply = f"🎤 {data['text']}\n\n{reply}"
delegated = herman.get("delegated_agents") or data.get("delegated_agents") or []
show = [a for a in delegated if str(a).lower() != "herman"]
if show:
await status_msg.edit_text(
f"🔄 Herman stuurt aan: {', '.join(show)}\n"
+ ((herman.get("routing_reason") or "")[:180])
)
reply += _format_delegation(herman)
reply += _format_ui_actions(data)
reply += _format_pending(data)
await update.message.reply_text(reply[:4000])
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.effective_chat:
save_chat_id(update.effective_chat.id)
await update.message.reply_text(
"Foodlinkk Herman — stel je vraag (tekst of voice). "
"Bij zoekopdrachten vraag ik eerst om bevestiging.\n/ping voor test."
)
async def cmd_ping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text("pong — Herman bereikbaar via Cockpit relay")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not update.message or not update.message.text:
return
chat_id = update.effective_chat.id if update.effective_chat else 0
save_chat_id(chat_id)
text = update.message.text.strip()
if not text:
return
await update.message.chat.send_action(ChatAction.TYPING)
status_msg = await update.message.reply_text("⏳ Herman denkt na…")
try:
data = await _call_herman(text, chat_id)
await _reply_herman_result(update, status_msg, data)
except Exception as exc:
log.exception("herman relay failed")
await status_msg.edit_text(f"Herman niet bereikbaar: {exc}")
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not update.message:
return
voice = update.message.voice or update.message.audio
if not voice:
return
chat_id = update.effective_chat.id if update.effective_chat else 0
save_chat_id(chat_id)
await update.message.chat.send_action(ChatAction.RECORD_VOICE)
status_msg = await update.message.reply_text("🎤 Voice → Whisper → Herman…")
try:
tg_file = await context.bot.get_file(voice.file_id)
buf = io.BytesIO()
await tg_file.download_to_memory(buf)
audio_bytes = buf.getvalue()
mime = "audio/ogg"
filename = "voice.ogg"
if update.message.audio and update.message.audio.mime_type:
mime = update.message.audio.mime_type
filename = "audio." + (mime.split("/")[-1] or "ogg")
data = await _call_voice_turn(audio_bytes, filename, mime, chat_id)
await _reply_herman_result(update, status_msg, data)
except Exception as exc:
log.exception("voice relay failed")
await status_msg.edit_text(f"Voice relay mislukt: {exc}")
def main() -> None:
app = Application.builder().token(TOKEN).build()
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("ping", cmd_ping))
app.add_handler(MessageHandler(filters.VOICE | filters.AUDIO, handle_voice))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
log.info("Hermes Telegram → chat %s | voice %s", COCKPIT_CHAT_URL, COCKPIT_VOICE_TURN_URL)
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Installeer Web Builder API op Hermes VM107 (10.4.7.27)
set -euo pipefail
HERMES="${HERMES_SSH:-root@10.4.7.27}"
PASS="${HERMES_SSH_PASS:-Foodlinkk#2026}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Hermes Web Builder API → $HERMES ==="
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no "$HERMES" 'mkdir -p /opt/webbuilder-api'
sshpass -p "$PASS" scp -o StrictHostKeyChecking=no \
"$SCRIPT_DIR/hermes-webbuilder-api.py" \
"$HERMES:/opt/webbuilder-api/main.py"
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no "$HERMES" bash -s <<'REMOTE'
set -euo pipefail
mkdir -p /opt/webbuilder-api
python3 -m pip install -q fastapi uvicorn 2>/dev/null || pip3 install -q fastapi uvicorn
cat > /etc/systemd/system/webbuilder-api.service <<'UNIT'
[Unit]
Description=Hermes Web Builder API (agy builds)
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/webbuilder-api
Environment=PORT=8798
Environment=WEBBUILDER_PROJECTS=/home/hermes/projects
Environment=WEBBUILDER_PREVIEW_HOST=10.4.7.27
Environment=WEBBUILDER_PREVIEW_PORT=8080
ExecStart=/usr/bin/python3 /opt/webbuilder-api/main.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable webbuilder-api
systemctl restart webbuilder-api
sleep 2
curl -sf http://127.0.0.1:8798/health && echo " webbuilder-api OK"
REMOTE
echo "=== Klaar: http://10.4.7.27:8798/health ==="
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""Publish due scheduled social posts."""
from __future__ import annotations
import json
import sys
import urllib.request
COCKPIT_URL = "http://127.0.0.1:8600"
TOOLS_URL = "http://127.0.0.1:8700"
def main() -> int:
try:
req = urllib.request.Request(
f"{COCKPIT_URL}/api/admin/scheduled-posts/publish-due",
data=b"{}",
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.loads(resp.read().decode("utf-8"))
print(json.dumps(data))
return 0
except urllib.error.HTTPError as exc:
if exc.code == 404:
print("publish-due endpoint not yet available — skipped")
return 0
print(f"publish failed: {exc}", file=sys.stderr)
return 1
except Exception as exc:
print(f"publish failed: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Voeg webbuilder toe aan Herman orchestrator op VM105
set -euo pipefail
ORCH_HOST="${ORCH_HOST:-aissa@10.4.7.19}"
MAIN="$HOME/foodlinkk-ai/herman-orchestrator/main.py"
ssh -o StrictHostKeyChecking=no "$ORCH_HOST" bash -s <<'REMOTE'
set -euo pipefail
MAIN=~/foodlinkk-ai/herman-orchestrator/main.py
cp "$MAIN" "${MAIN}.bak.$(date +%Y%m%d%H%M)"
python3 <<'PY'
from pathlib import Path
p = Path.home() / "foodlinkk-ai/herman-orchestrator/main.py"
text = p.read_text()
if "webbuilder" in text and '"webbuilder"' in text:
print("webbuilder already present")
else:
text = text.replace(
' "browser",\n)',
' "browser",\n "webbuilder",\n)',
)
insert = ''' "webbuilder": {
"name": "Web Builder",
"persona": (
"Website development via Hermes/agy on NAS. HTML/CSS/JS sites, "
"landing pages, preview servers. Reports back to Herman when done."
),
"keywords": "website,bouw website,maak website,landingspagina,webbuilder,webpagina,nieuwe site",
},
'''
marker = ' "browser": {'
if insert.strip() not in text:
text = text.replace(marker, insert + marker, 1)
text = text.replace(
'"keywords": "website,browser,site,monitor,crawl,bidfood,web,url",',
'"keywords": "browser,site,monitor,crawl,bidfood,url,concurrent,scrape",',
)
p.write_text(text)
print("patched orchestrator main.py")
PY
# Restart orchestrator if systemd service exists
if systemctl is-active herman-orchestrator >/dev/null 2>&1; then
sudo systemctl restart herman-orchestrator
elif pgrep -f "herman-orchestrator/main.py" >/dev/null; then
pkill -f "herman-orchestrator/main.py" || true
sleep 1
cd ~/foodlinkk-ai/herman-orchestrator && nohup python3 -m uvicorn main:app --host 0.0.0.0 --port 8090 >> /tmp/herman-orchestrator.log 2>&1 &
fi
sleep 2
curl -sf http://127.0.0.1:8090/health | python3 -c "import sys,json; d=json.load(sys.stdin); print('agents:', d.get('agents'))"
REMOTE
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Push volledige foodlinkk-command-center naar Gitea (VM106).
set -euo pipefail
HOST="${DEPLOY_HOST:-aissa@10.4.7.18}"
REASON="${1:-manual-backup}"
echo "=== Rsync naar VM106 ==="
rsync -az --exclude '.git' --exclude '__pycache__' --exclude 'node_modules' \
"$(dirname "$0")/../" "${HOST}:~/foodlinkk-command-center/"
echo "=== Gitea sync via tools-api ==="
ssh "$HOST" "docker exec foodlinkk_tools_api python -c \"
from app.connectors.gitea_repo_sync import run_gitea_repo_sync
import json
print(json.dumps(run_gitea_repo_sync('${REASON}'), indent=2))
\""
echo "=== Klaar — Gitea: http://10.4.7.18:3001/aissa/foodlinkk-command-center ==="