Chris4K commited on
Commit
f6c5cd8
·
verified ·
1 Parent(s): 4e4bf46

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +393 -6
main.py CHANGED
@@ -14,6 +14,7 @@ Capability types:
14
  """
15
 
16
  import asyncio
 
17
  import json
18
  import os
19
  import sqlite3
@@ -32,10 +33,12 @@ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
32
  # Config
33
  # ---------------------------------------------------------------------------
34
 
35
- DB_PATH = Path(os.getenv("FORGE_DB", "/tmp/forge.db"))
36
- PORT = int(os.getenv("PORT", "7860"))
37
- FORGE_KEY = os.getenv("FORGE_KEY", "") # optional publish auth key
38
- BASE_URL = os.getenv("FORGE_BASE_URL", "https://chris4k-agent-forge.hf.space")
 
 
39
 
40
  VALID_TYPES = {
41
  "skill", "prompt", "workflow", "knowledge",
@@ -897,6 +900,173 @@ async def api_tags():
897
  return JSONResponse({"tags": sorted(tags)})
898
 
899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  @app.get("/api/health")
901
  async def health():
902
  stats = db_stats()
@@ -1134,6 +1304,7 @@ pre{background:#0a0a14;border:1px solid var(--border);border-radius:6px;padding:
1134
  <button class="type-btn" onclick="showTab('browse')">&#128270; Browse</button>
1135
  <button class="type-btn" onclick="showTab('publish')">&#128228; Publish</button>
1136
  <button class="type-btn" onclick="showTab('api')">&#128225; API Docs</button>
 
1137
  </nav>
1138
 
1139
  <div class="content">
@@ -1210,6 +1381,87 @@ pre{background:#0a0a14;border:1px solid var(--border);border-radius:6px;padding:
1210
  <div class="result-msg" id="pubResult"></div>
1211
  </div>
1212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  <!-- API TAB -->
1214
  <div id="tab-api" style="display:none">
1215
  <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">FORGE API v2</h2>
@@ -1466,9 +1718,10 @@ function showList() {
1466
  }
1467
 
1468
  function showTab(tab) {
1469
- ['browse','publish','api'].forEach(t => {
1470
  document.getElementById('tab-'+t).style.display = t===tab?'block':'none';
1471
  });
 
1472
  }
1473
 
1474
  // Publish
@@ -1552,6 +1805,140 @@ function escHtml(s) {
1552
 
1553
  // Init
1554
  loadStats();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
  doSearch();
1556
  </script>
1557
  </body>
@@ -1568,4 +1955,4 @@ async def root():
1568
  # ---------------------------------------------------------------------------
1569
 
1570
  if __name__ == "__main__":
1571
- uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
 
14
  """
15
 
16
  import asyncio
17
+ import httpx
18
  import json
19
  import os
20
  import sqlite3
 
33
  # Config
34
  # ---------------------------------------------------------------------------
35
 
36
+ DB_PATH = Path(os.getenv("FORGE_DB", "/tmp/forge.db"))
37
+ PORT = int(os.getenv("PORT", "7860"))
38
+ FORGE_KEY = os.getenv("FORGE_KEY", "")
39
+ BASE_URL = os.getenv("FORGE_BASE_URL", "https://chris4k-agent-forge.hf.space")
40
+ PULSE_URL = os.getenv("PULSE_URL", "https://chris4k-agent-pulse.hf.space")
41
+ PROMPTS_URL = os.getenv("PROMPTS_URL", "https://chris4k-agent-prompts.hf.space")
42
 
43
  VALID_TYPES = {
44
  "skill", "prompt", "workflow", "knowledge",
 
900
  return JSONResponse({"tags": sorted(tags)})
901
 
902
 
903
+ # ---------------------------------------------------------------------------
904
+ # Agent Config Handler — Sprint 5
905
+ # ---------------------------------------------------------------------------
906
+
907
+ AGENT_DEFAULTS = {
908
+ "heartbeat_seconds": 0,
909
+ "cost_mode": "balanced",
910
+ "max_react_steps": 6,
911
+ "color": "#ff6b00",
912
+ "tags": [],
913
+ "enabled": True,
914
+ }
915
+
916
+ COST_MODES = ["cheap", "balanced", "best"]
917
+ AGENT_COLORS = ["#ff6b00","#0ea5e9","#2ed573","#ff9500","#ff6b9d","#8b5cf6","#10b981","#f59e0b"]
918
+
919
+ async def _push(url: str, path: str, payload: dict) -> dict:
920
+ try:
921
+ async with httpx.AsyncClient(timeout=8) as c:
922
+ r = await c.post(url + path, json=payload)
923
+ r.raise_for_status()
924
+ return r.json()
925
+ except Exception as e:
926
+ return {"ok": False, "error": str(e)}
927
+
928
+ async def _patch(url: str, path: str, payload: dict) -> dict:
929
+ try:
930
+ async with httpx.AsyncClient(timeout=8) as c:
931
+ r = await c.patch(url + path, json=payload)
932
+ r.raise_for_status()
933
+ return r.json()
934
+ except Exception as e:
935
+ return {"ok": False, "error": str(e)}
936
+
937
+ async def _get(url: str, path: str) -> dict | list | None:
938
+ try:
939
+ async with httpx.AsyncClient(timeout=6) as c:
940
+ r = await c.get(url + path)
941
+ r.raise_for_status()
942
+ return r.json()
943
+ except Exception:
944
+ return None
945
+
946
+
947
+ @app.post("/api/agents/register", status_code=201)
948
+ async def register_agent(request: Request):
949
+ """
950
+ One-shot agent registration.
951
+ Pushes to PULSE (agent config) AND agent-prompts (persona) simultaneously.
952
+
953
+ Body:
954
+ name str required — agent identifier (slug)
955
+ persona str required — system prompt / persona description
956
+ heartbeat_seconds int 0=manual only
957
+ cost_mode str cheap | balanced | best
958
+ max_react_steps int
959
+ color str hex colour for UI
960
+ tags list[str]
961
+ enabled bool
962
+ """
963
+ body = await request.json()
964
+ name = (body.get("name") or "").strip().lower().replace(" ", "_")
965
+ if not name:
966
+ raise HTTPException(status_code=400, detail="name is required")
967
+
968
+ persona = (body.get("persona") or "").strip()
969
+ if not persona:
970
+ raise HTTPException(status_code=400, detail="persona is required")
971
+
972
+ agent_cfg = {
973
+ "name": name,
974
+ "persona": persona,
975
+ "heartbeat_seconds": int(body.get("heartbeat_seconds", 0)),
976
+ "cost_mode": body.get("cost_mode", "balanced"),
977
+ "max_react_steps": int(body.get("max_react_steps", 6)),
978
+ "color": body.get("color", "#ff6b00"),
979
+ "tags": body.get("tags", []),
980
+ "enabled": bool(body.get("enabled", True)),
981
+ }
982
+
983
+ results = {}
984
+
985
+ # 1. Push agent config to PULSE
986
+ pulse_r = await _push(PULSE_URL, "/api/agents", agent_cfg)
987
+ results["pulse"] = pulse_r
988
+
989
+ # 2. Push persona to agent-prompts
990
+ prompts_payload = {
991
+ "agent": name,
992
+ "name": f"{name.upper()} Agent",
993
+ "system_prompt": persona,
994
+ "model_pref": agent_cfg["cost_mode"],
995
+ "max_steps": agent_cfg["max_react_steps"],
996
+ "tools": [],
997
+ "config": {"color": agent_cfg["color"], "tags": agent_cfg["tags"]},
998
+ }
999
+ prompts_r = await _push(PROMPTS_URL, "/api/personas", prompts_payload)
1000
+ results["prompts"] = prompts_r
1001
+
1002
+ # 3. Also store as a FORGE config capability (self-registry)
1003
+ cap_id = f"agent_config_{name}"
1004
+ try:
1005
+ conn = get_db()
1006
+ now = int(time.time())
1007
+ vid = str(uuid.uuid4())[:8]
1008
+ conn.execute("""
1009
+ INSERT OR REPLACE INTO capabilities
1010
+ (id, name, description, type, payload, author, tags, version,
1011
+ created_at, updated_at, verified, download_count)
1012
+ VALUES (?,?,?,?,?,?,?,?,?,?,0,0)
1013
+ """, (cap_id,
1014
+ f"Agent Config: {name}",
1015
+ f"Registered agent configuration for {name}",
1016
+ "config",
1017
+ json.dumps(agent_cfg),
1018
+ "forge-config-handler",
1019
+ json.dumps(["agent", name, "config"]),
1020
+ vid, now, now))
1021
+ conn.commit()
1022
+ results["forge_cap"] = cap_id
1023
+ except Exception as e:
1024
+ results["forge_cap"] = f"warning: {e}"
1025
+
1026
+ pulse_ok = isinstance(pulse_r, dict) and "error" not in pulse_r
1027
+ prompts_ok = isinstance(prompts_r, dict) and "error" not in prompts_r
1028
+ all_ok = pulse_ok or prompts_ok # partial success is still useful
1029
+
1030
+ return JSONResponse(
1031
+ status_code=201 if all_ok else 207,
1032
+ content={"ok": all_ok, "agent": name, "results": results}
1033
+ )
1034
+
1035
+
1036
+ @app.get("/api/agents")
1037
+ async def list_agents():
1038
+ """Proxy PULSE /api/agents list. Falls back to FORGE config store."""
1039
+ live = await _get(PULSE_URL, "/api/agents")
1040
+ if live is not None:
1041
+ return JSONResponse(live if isinstance(live, list) else [live])
1042
+ # Fallback: pull from forge config store
1043
+ conn = get_db()
1044
+ rows = conn.execute(
1045
+ "SELECT payload FROM capabilities WHERE type='config' AND id LIKE 'agent_config_%'"
1046
+ ).fetchall()
1047
+ agents = []
1048
+ for row in rows:
1049
+ try: agents.append(json.loads(row["payload"]))
1050
+ except Exception: pass
1051
+ return JSONResponse(agents)
1052
+
1053
+
1054
+ @app.delete("/api/agents/{agent_name}")
1055
+ async def delete_agent(agent_name: str):
1056
+ """Disable an agent in PULSE."""
1057
+ r = await _push(PULSE_URL, f"/api/agents/{agent_name}/disable", {})
1058
+ return JSONResponse({"ok": True, "pulse": r})
1059
+
1060
+
1061
+ @app.post("/api/agents/{agent_name}/trigger")
1062
+ async def trigger_agent(agent_name: str, request: Request):
1063
+ """Manually trigger an agent tick via PULSE."""
1064
+ body = await request.json()
1065
+ r = await _push(PULSE_URL, f"/api/trigger/{agent_name}", body)
1066
+ return JSONResponse({"ok": True, "pulse": r})
1067
+
1068
+
1069
+
1070
  @app.get("/api/health")
1071
  async def health():
1072
  stats = db_stats()
 
1304
  <button class="type-btn" onclick="showTab('browse')">&#128270; Browse</button>
1305
  <button class="type-btn" onclick="showTab('publish')">&#128228; Publish</button>
1306
  <button class="type-btn" onclick="showTab('api')">&#128225; API Docs</button>
1307
+ <button class="type-btn" onclick="showTab('agents')">&#129302; Agents</button>
1308
  </nav>
1309
 
1310
  <div class="content">
 
1381
  <div class="result-msg" id="pubResult"></div>
1382
  </div>
1383
 
1384
+ <!-- AGENTS TAB -->
1385
+ <div id="tab-agents" style="display:none">
1386
+ <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">&#129302; Agent Config Handler</h2>
1387
+ <p style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);margin-bottom:1.5rem">
1388
+ Register or update an agent in PULSE + agent-prompts in one shot.
1389
+ </p>
1390
+
1391
+ <!-- Live agents grid -->
1392
+ <div style="margin-bottom:1.5rem">
1393
+ <div style="display:flex;align-items:center;gap:1rem;margin-bottom:.75rem">
1394
+ <span style="font-family:'DM Mono',monospace;font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.15em">Live Agents</span>
1395
+ <button class="btn btn-secondary" id="refreshAgentsBtn" style="padding:.25rem .75rem;font-size:.75rem">&#8635; Refresh</button>
1396
+ </div>
1397
+ <div id="agentGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.75rem"></div>
1398
+ </div>
1399
+
1400
+ <!-- Registration form -->
1401
+ <div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:1.5rem;max-width:720px">
1402
+ <h3 style="font-family:'Space Mono',monospace;font-size:.9rem;color:var(--accent);margin-bottom:1.25rem">&#43; Register / Update Agent</h3>
1403
+
1404
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
1405
+ <div>
1406
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Agent Name (slug) *</label>
1407
+ <input id="ag-name" class="search-input" placeholder="researcher" style="width:100%">
1408
+ </div>
1409
+ <div>
1410
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Cost Mode</label>
1411
+ <select id="ag-cost" style="width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:.5rem .75rem;border-radius:4px;font-family:'DM Mono',monospace;font-size:.8rem">
1412
+ <option value="cheap">cheap (fast)</option>
1413
+ <option value="balanced" selected>balanced</option>
1414
+ <option value="best">best (slow)</option>
1415
+ </select>
1416
+ </div>
1417
+ </div>
1418
+
1419
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
1420
+ <div>
1421
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Heartbeat (seconds, 0=manual)</label>
1422
+ <input id="ag-hb" type="number" min="0" value="0" class="search-input" style="width:100%">
1423
+ </div>
1424
+ <div>
1425
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Max ReAct Steps</label>
1426
+ <input id="ag-steps" type="number" min="1" max="20" value="6" class="search-input" style="width:100%">
1427
+ </div>
1428
+ </div>
1429
+
1430
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem">
1431
+ <div>
1432
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Tags (comma-separated)</label>
1433
+ <input id="ag-tags" class="search-input" placeholder="research,analysis" style="width:100%">
1434
+ </div>
1435
+ <div>
1436
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">UI Color</label>
1437
+ <div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;margin-top:.2rem">
1438
+ <input type="color" id="ag-color" value="#ff6b00" style="width:36px;height:36px;border:none;background:none;cursor:pointer;padding:0">
1439
+ <span style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted)" id="ag-color-lbl">#ff6b00</span>
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+
1444
+ <div style="margin-bottom:1rem">
1445
+ <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Persona / System Prompt *</label>
1446
+ <textarea id="ag-persona" class="search-input" rows="6"
1447
+ placeholder="You are a deep research specialist. Your job is to..."
1448
+ style="width:100%;resize:vertical;min-height:120px;font-family:'DM Mono',monospace;font-size:.8rem;line-height:1.5"></textarea>
1449
+ </div>
1450
+
1451
+ <div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem">
1452
+ <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:.78rem;color:var(--muted)">
1453
+ <input type="checkbox" id="ag-enabled" checked style="accent-color:var(--accent)"> Enabled
1454
+ </label>
1455
+ </div>
1456
+
1457
+ <div style="display:flex;gap:.75rem;align-items:center">
1458
+ <button class="btn btn-primary" id="agRegisterBtn">&#128640; Register Agent</button>
1459
+ <button class="btn btn-secondary" id="agClearBtn">Clear</button>
1460
+ <span id="agResult" style="font-family:'DM Mono',monospace;font-size:.78rem"></span>
1461
+ </div>
1462
+ </div>
1463
+ </div>
1464
+
1465
  <!-- API TAB -->
1466
  <div id="tab-api" style="display:none">
1467
  <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">FORGE API v2</h2>
 
1718
  }
1719
 
1720
  function showTab(tab) {
1721
+ ['browse','publish','api','agents'].forEach(t => {
1722
  document.getElementById('tab-'+t).style.display = t===tab?'block':'none';
1723
  });
1724
+ if (tab === 'agents') loadAgents();
1725
  }
1726
 
1727
  // Publish
 
1805
 
1806
  // Init
1807
  loadStats();
1808
+ // ── Agents tab ──────────────────────────────────────────────────────
1809
+
1810
+ async function loadAgents() {
1811
+ const grid = document.getElementById('agentGrid');
1812
+ grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">Loading&#8230;</span>';
1813
+ try {
1814
+ const r = await fetch('/api/agents');
1815
+ const agents = await r.json();
1816
+ if (!agents || !agents.length) {
1817
+ grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">No agents registered yet.</span>';
1818
+ return;
1819
+ }
1820
+ grid.innerHTML = agents.map(a => {
1821
+ const color = a.color || '#ff6b00';
1822
+ const enabled = a.enabled !== false;
1823
+ return `<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ${color};border-radius:6px;padding:.85rem">
1824
+ <div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem">
1825
+ <span style="width:8px;height:8px;border-radius:50%;background:${enabled?'#00ff88':'#ef4444'};flex-shrink:0"></span>
1826
+ <span style="font-family:'Space Mono',monospace;font-size:.82rem;font-weight:700;color:${color}">${esc(a.name||'?')}</span>
1827
+ </div>
1828
+ <div style="font-family:'DM Mono',monospace;font-size:.68rem;color:var(--muted);margin-bottom:.5rem;line-height:1.4">${esc((a.persona||'').slice(0,100))}${(a.persona||'').length>100?'&#8230;':''}</div>
1829
+ <div style="display:flex;gap:.5rem;flex-wrap:wrap">
1830
+ <span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">${esc(a.cost_mode||'balanced')}</span>
1831
+ <span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">steps:${a.max_react_steps||6}</span>
1832
+ ${a.heartbeat_seconds?`<span style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);padding:.1rem .4rem;border-radius:3px;color:var(--muted)">hb:${a.heartbeat_seconds}s</span>`:''}
1833
+ </div>
1834
+ <div style="margin-top:.6rem;display:flex;gap:.4rem">
1835
+ <button onclick="triggerAgent('${esc(a.name||'')}')" style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);color:var(--accent);padding:.2rem .5rem;border-radius:3px;cursor:pointer">&#9654; Trigger</button>
1836
+ <button onclick="prefillAgent(${JSON.stringify(a)})" style="font-family:'DM Mono',monospace;font-size:.65rem;background:var(--surface);border:1px solid var(--border);color:var(--muted);padding:.2rem .5rem;border-radius:3px;cursor:pointer">&#9998; Edit</button>
1837
+ </div>
1838
+ </div>`;
1839
+ }).join('');
1840
+ } catch(e) {
1841
+ grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--red)">Error loading agents: ' + esc(String(e)) + '</span>';
1842
+ }
1843
+ }
1844
+
1845
+ async function registerAgent() {
1846
+ const name = document.getElementById('ag-name').value.trim();
1847
+ const persona = document.getElementById('ag-persona').value.trim();
1848
+ if (!name || !persona) {
1849
+ setAgResult('error', 'Name and persona are required.');
1850
+ return;
1851
+ }
1852
+ const tagsRaw = document.getElementById('ag-tags').value.trim();
1853
+ const tags = tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : [];
1854
+ const payload = {
1855
+ name: name,
1856
+ persona: persona,
1857
+ heartbeat_seconds: parseInt(document.getElementById('ag-hb').value)||0,
1858
+ cost_mode: document.getElementById('ag-cost').value,
1859
+ max_react_steps: parseInt(document.getElementById('ag-steps').value)||6,
1860
+ color: document.getElementById('ag-color').value,
1861
+ tags: tags,
1862
+ enabled: document.getElementById('ag-enabled').checked,
1863
+ };
1864
+ const btn = document.getElementById('agRegisterBtn');
1865
+ btn.disabled = true;
1866
+ btn.textContent = 'Registering&#8230;';
1867
+ setAgResult('muted','Pushing to PULSE + agent-prompts&#8230;');
1868
+ try {
1869
+ const r = await fetch('/api/agents/register', {
1870
+ method:'POST', headers:{'Content-Type':'application/json'},
1871
+ body: JSON.stringify(payload)
1872
+ });
1873
+ const d = await r.json();
1874
+ if (d.ok) {
1875
+ setAgResult('green', '&#10003; Agent &ldquo;' + esc(name) + '&rdquo; registered!');
1876
+ clearAgentForm();
1877
+ setTimeout(loadAgents, 800);
1878
+ } else {
1879
+ setAgResult('red', '&#10007; ' + JSON.stringify(d.results).slice(0,200));
1880
+ }
1881
+ } catch(e) {
1882
+ setAgResult('red', 'Network error: ' + esc(String(e)));
1883
+ } finally {
1884
+ btn.disabled = false;
1885
+ btn.textContent = '&#128640; Register Agent';
1886
+ }
1887
+ }
1888
+
1889
+ async function triggerAgent(name) {
1890
+ try {
1891
+ const r = await fetch('/api/agents/' + encodeURIComponent(name) + '/trigger', {
1892
+ method:'POST', headers:{'Content-Type':'application/json'},
1893
+ body: JSON.stringify({content:'Manual trigger from FORGE UI', trigger_type:'manual'})
1894
+ });
1895
+ const d = await r.json();
1896
+ setAgResult(d.ok?'green':'red', d.ok ? '&#9654; Triggered ' + esc(name) : 'Trigger failed: ' + JSON.stringify(d));
1897
+ } catch(e) { setAgResult('red', String(e)); }
1898
+ }
1899
+
1900
+ function prefillAgent(a) {
1901
+ document.getElementById('ag-name').value = a.name || '';
1902
+ document.getElementById('ag-persona').value = a.persona || '';
1903
+ document.getElementById('ag-hb').value = a.heartbeat_seconds || 0;
1904
+ document.getElementById('ag-cost').value = a.cost_mode || 'balanced';
1905
+ document.getElementById('ag-steps').value = a.max_react_steps || 6;
1906
+ document.getElementById('ag-color').value = a.color || '#ff6b00';
1907
+ document.getElementById('ag-color-lbl').textContent = a.color || '#ff6b00';
1908
+ document.getElementById('ag-tags').value = (a.tags||[]).join(', ');
1909
+ document.getElementById('ag-enabled').checked = a.enabled !== false;
1910
+ document.getElementById('agRegisterBtn').scrollIntoView({behavior:'smooth',block:'nearest'});
1911
+ }
1912
+
1913
+ function clearAgentForm() {
1914
+ ['ag-name','ag-persona','ag-tags'].forEach(id => document.getElementById(id).value = '');
1915
+ document.getElementById('ag-hb').value = '0';
1916
+ document.getElementById('ag-cost').value = 'balanced';
1917
+ document.getElementById('ag-steps').value = '6';
1918
+ document.getElementById('ag-color').value = '#ff6b00';
1919
+ document.getElementById('ag-color-lbl').textContent = '#ff6b00';
1920
+ document.getElementById('ag-enabled').checked = true;
1921
+ setAgResult('','');
1922
+ }
1923
+
1924
+ function setAgResult(type, msg) {
1925
+ const el = document.getElementById('agResult');
1926
+ const colors = {green:'var(--green)',red:'var(--red)',muted:'var(--muted)',error:'var(--red)','':`var(--text)`};
1927
+ el.style.color = colors[type] || 'var(--text)';
1928
+ el.innerHTML = msg;
1929
+ }
1930
+
1931
+ // Wire up listeners once DOM ready
1932
+ document.addEventListener('DOMContentLoaded', () => {
1933
+ document.getElementById('agRegisterBtn').addEventListener('click', registerAgent);
1934
+ document.getElementById('agClearBtn').addEventListener('click', clearAgentForm);
1935
+ document.getElementById('refreshAgentsBtn').addEventListener('click', loadAgents);
1936
+ document.getElementById('ag-color').addEventListener('input', e => {
1937
+ document.getElementById('ag-color-lbl').textContent = e.target.value;
1938
+ });
1939
+ });
1940
+
1941
+
1942
  doSearch();
1943
  </script>
1944
  </body>
 
1955
  # ---------------------------------------------------------------------------
1956
 
1957
  if __name__ == "__main__":
1958
+ uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")