Spaces:
Running
Running
Update main.py
Browse files
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
|
| 36 |
-
PORT
|
| 37 |
-
FORGE_KEY
|
| 38 |
-
BASE_URL
|
|
|
|
|
|
|
| 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')">🔎 Browse</button>
|
| 1135 |
<button class="type-btn" onclick="showTab('publish')">📤 Publish</button>
|
| 1136 |
<button class="type-btn" onclick="showTab('api')">📡 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')">🔎 Browse</button>
|
| 1305 |
<button class="type-btn" onclick="showTab('publish')">📤 Publish</button>
|
| 1306 |
<button class="type-btn" onclick="showTab('api')">📡 API Docs</button>
|
| 1307 |
+
<button class="type-btn" onclick="showTab('agents')">🤖 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">🤖 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">↻ 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">+ 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">🚀 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…</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?'…':''}</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">▶ 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">✎ 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…';
|
| 1867 |
+
setAgResult('muted','Pushing to PULSE + agent-prompts…');
|
| 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', '✓ Agent “' + esc(name) + '” registered!');
|
| 1876 |
+
clearAgentForm();
|
| 1877 |
+
setTimeout(loadAgents, 800);
|
| 1878 |
+
} else {
|
| 1879 |
+
setAgResult('red', '✗ ' + 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 = '🚀 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 ? '▶ 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")
|