Spaces:
Sleeping
Sleeping
| """ | |
| FORGE v2 - Universal Capability Registry for AI Agents | |
| Federated Open Registry for Generative Executables (and more) | |
| Capability types: | |
| skill - executable Python code (def execute(...)) | |
| prompt - system prompts, personas, jinja2 templates | |
| workflow - multi-step ReAct plans / orchestration graphs | |
| knowledge - curated text/JSON for RAG injection | |
| config - agent behavior policies, guardrails | |
| mcp_ref - pointer to external MCP server | |
| model_ref - HF model pointer + prompting guide | |
| bundle - named collection of capabilities (agent loadout) | |
| """ | |
| import asyncio | |
| import httpx | |
| import json | |
| import os | |
| import sqlite3 | |
| import time | |
| import uuid | |
| from contextlib import asynccontextmanager | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Optional | |
| import uvicorn | |
| from fastapi import FastAPI, HTTPException, Query, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| DB_PATH = Path(os.getenv("FORGE_DB", "/tmp/forge.db")) | |
| PORT = int(os.getenv("PORT", "7860")) | |
| FORGE_KEY = os.getenv("FORGE_KEY", "") | |
| BASE_URL = os.getenv("FORGE_BASE_URL", "https://chris4k-agent-forge.hf.space") | |
| PULSE_URL = os.getenv("PULSE_URL", "https://chris4k-agent-pulse.hf.space") | |
| PROMPTS_URL = os.getenv("PROMPTS_URL", "https://chris4k-agent-prompts.hf.space") | |
| VALID_TYPES = { | |
| "skill", "prompt", "workflow", "knowledge", | |
| "config", "mcp_ref", "model_ref", "bundle" | |
| } | |
| TYPE_ICONS = { | |
| "skill": "⚙", # gear | |
| "prompt": "💬", # speech bubble | |
| "workflow": "🔗", # link | |
| "knowledge": "📚", # book | |
| "config": "⚙︎", # settings | |
| "mcp_ref": "📡", # antenna | |
| "model_ref": "🤖", # robot | |
| "bundle": "📦", # package | |
| } | |
| TYPE_COLORS = { | |
| "skill": "#ff6b00", | |
| "prompt": "#8b5cf6", | |
| "workflow": "#06b6d4", | |
| "knowledge": "#10b981", | |
| "config": "#f59e0b", | |
| "mcp_ref": "#ef4444", | |
| "model_ref": "#ec4899", | |
| "bundle": "#6366f1", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Database | |
| # --------------------------------------------------------------------------- | |
| def get_db() -> sqlite3.Connection: | |
| conn = sqlite3.connect(str(DB_PATH), check_same_thread=False) | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| conn.execute("PRAGMA foreign_keys=ON") | |
| return conn | |
| def init_db(): | |
| conn = get_db() | |
| conn.executescript(""" | |
| CREATE TABLE IF NOT EXISTS capabilities ( | |
| id TEXT NOT NULL, | |
| version TEXT NOT NULL DEFAULT '1.0.0', | |
| type TEXT NOT NULL, | |
| name TEXT NOT NULL, | |
| description TEXT NOT NULL DEFAULT '', | |
| author TEXT NOT NULL DEFAULT 'anonymous', | |
| tags TEXT NOT NULL DEFAULT '[]', | |
| payload TEXT NOT NULL DEFAULT '{}', | |
| schema_in TEXT NOT NULL DEFAULT '{}', | |
| schema_out TEXT NOT NULL DEFAULT '{}', | |
| deps TEXT NOT NULL DEFAULT '[]', | |
| meta TEXT NOT NULL DEFAULT '{}', | |
| downloads INTEGER NOT NULL DEFAULT 0, | |
| deprecated INTEGER NOT NULL DEFAULT 0, | |
| created_at REAL NOT NULL, | |
| updated_at REAL NOT NULL, | |
| PRIMARY KEY (id, version) | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_cap_type ON capabilities(type); | |
| CREATE INDEX IF NOT EXISTS idx_cap_created ON capabilities(created_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_cap_downloads ON capabilities(downloads DESC); | |
| CREATE TABLE IF NOT EXISTS events ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| kind TEXT NOT NULL, | |
| cap_id TEXT, | |
| cap_type TEXT, | |
| meta TEXT NOT NULL DEFAULT '{}', | |
| ts REAL NOT NULL | |
| ); | |
| """) | |
| conn.commit() | |
| conn.close() | |
| def seed_db(): | |
| """Seed with built-in capabilities if DB is empty.""" | |
| conn = get_db() | |
| count = conn.execute("SELECT COUNT(*) FROM capabilities").fetchone()[0] | |
| if count > 0: | |
| conn.close() | |
| return | |
| now = time.time() | |
| seeds = [ | |
| # --- skills --- | |
| { | |
| "id": "forge_client", | |
| "version": "2.0.0", | |
| "type": "skill", | |
| "name": "FORGE Client", | |
| "description": "Bootstrap client. Download this first. Lets agents discover, hot-load, and publish capabilities at runtime.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["meta", "core", "bootstrap"]), | |
| "payload": json.dumps({ | |
| "language": "python", | |
| "dependencies": ["requests"], | |
| "code": ( | |
| "import requests, types, sys\n" | |
| "from typing import Optional\n\n" | |
| "class ForgeClient:\n" | |
| " def __init__(self, url='https://chris4k-agent-forge.hf.space'):\n" | |
| " self.url = url.rstrip('/')\n" | |
| " self._cache = {}\n\n" | |
| " def search(self, q='', cap_type=None, tag=None, limit=20):\n" | |
| " params = {'q': q, 'limit': limit}\n" | |
| " if cap_type: params['type'] = cap_type\n" | |
| " if tag: params['tag'] = tag\n" | |
| " return requests.get(f'{self.url}/api/capabilities', params=params, timeout=10).json()\n\n" | |
| " def get(self, cap_id, version=None):\n" | |
| " path = f'{self.url}/api/capabilities/{cap_id}'\n" | |
| " if version: path += f'/{version}'\n" | |
| " return requests.get(path, timeout=10).json()\n\n" | |
| " def load_skill(self, cap_id, force=False):\n" | |
| " if cap_id in self._cache and not force:\n" | |
| " return self._cache[cap_id]\n" | |
| " r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n" | |
| " m = types.ModuleType(f'forge_{cap_id}')\n" | |
| " exec(compile(r['payload']['code'], f'<forge:{cap_id}>', 'exec'), m.__dict__)\n" | |
| " if not hasattr(m, 'execute'):\n" | |
| " raise ImportError(f'Skill {cap_id!r} has no execute() function')\n" | |
| " self._cache[cap_id] = m\n" | |
| " return m\n\n" | |
| " def get_prompt(self, cap_id, variables=None):\n" | |
| " r = requests.get(f'{self.url}/api/capabilities/{cap_id}/payload', timeout=10).json()\n" | |
| " tmpl = r['payload']['template']\n" | |
| " if variables:\n" | |
| " for k, v in variables.items():\n" | |
| " tmpl = tmpl.replace('{{' + k + '}}', str(v))\n" | |
| " return tmpl\n\n" | |
| " def get_bundle(self, bundle_id):\n" | |
| " return requests.get(f'{self.url}/api/capabilities/{bundle_id}/resolve', timeout=15).json()\n\n" | |
| " def publish(self, capability, api_key=None):\n" | |
| " h = {'Content-Type': 'application/json'}\n" | |
| " if api_key: h['X-Forge-Key'] = api_key\n" | |
| " return requests.post(f'{self.url}/api/capabilities', json=capability, headers=h, timeout=15).json()\n\n" | |
| "def execute(url='https://chris4k-agent-forge.hf.space'):\n" | |
| " c = ForgeClient(url)\n" | |
| " stats = requests.get(f'{url}/api/stats', timeout=5).json()\n" | |
| " return {'client': c, 'stats': stats, 'message': f'FORGE ready. {stats.get(\"total\",0)} capabilities.'}\n" | |
| ) | |
| }), | |
| "schema_in": json.dumps({"url": "str - FORGE base URL"}), | |
| "schema_out": json.dumps({"client": "ForgeClient", "stats": "dict"}), | |
| "deps": json.dumps([]), | |
| }, | |
| { | |
| "id": "calculator", | |
| "version": "1.0.0", | |
| "type": "skill", | |
| "name": "Calculator", | |
| "description": "Safe math expression evaluator. Supports arithmetic, powers, trig, logs. Uses AST parsing - no eval() risks.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["math", "utility"]), | |
| "payload": json.dumps({ | |
| "language": "python", | |
| "dependencies": [], | |
| "code": ( | |
| "import ast, math, operator as op\n\n" | |
| "_OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,\n" | |
| " ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg,\n" | |
| " ast.Mod: op.mod, ast.FloorDiv: op.floordiv}\n" | |
| "_NAMES = {'pi': math.pi, 'e': math.e, 'inf': math.inf, 'tau': math.tau,\n" | |
| " 'sqrt': math.sqrt, 'abs': abs, 'round': round, 'floor': math.floor,\n" | |
| " 'ceil': math.ceil, 'log': math.log, 'log2': math.log2, 'log10': math.log10,\n" | |
| " 'exp': math.exp, 'sin': math.sin, 'cos': math.cos, 'tan': math.tan,\n" | |
| " 'factorial': math.factorial, 'min': min, 'max': max}\n\n" | |
| "def _eval(node):\n" | |
| " if isinstance(node, ast.Constant): return node.value\n" | |
| " if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_eval(node.left), _eval(node.right))\n" | |
| " if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_eval(node.operand))\n" | |
| " if isinstance(node, ast.Call):\n" | |
| " fn = _NAMES.get(node.func.id)\n" | |
| " if not fn: raise ValueError(f'Unknown function: {node.func.id}')\n" | |
| " return fn(*[_eval(a) for a in node.args])\n" | |
| " if isinstance(node, ast.Name): return _NAMES[node.id]\n" | |
| " if isinstance(node, ast.Expression): return _eval(node.body)\n" | |
| " raise ValueError(f'Unsupported: {type(node).__name__}')\n\n" | |
| "def execute(expression: str) -> dict:\n" | |
| " try:\n" | |
| " result = _eval(ast.parse(expression.strip(), mode='eval'))\n" | |
| " return {'result': float(result), 'formatted': f'{result:.10g}', 'expression': expression}\n" | |
| " except Exception as exc:\n" | |
| " return {'error': str(exc), 'expression': expression}\n" | |
| ) | |
| }), | |
| "schema_in": json.dumps({"expression": "str - math expression"}), | |
| "schema_out": json.dumps({"result": "float", "formatted": "str"}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- prompts --- | |
| { | |
| "id": "researcher_persona", | |
| "version": "1.0.0", | |
| "type": "prompt", | |
| "name": "Researcher Persona", | |
| "description": "System prompt for a deep-research agent with self-improvement duty. Includes ReAct discipline, pattern detection, skill candidate identification.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["persona", "researcher", "react", "self-improvement"]), | |
| "payload": json.dumps({ | |
| "format": "plain", | |
| "variables": ["agent_name", "max_steps"], | |
| "template": ( | |
| "You are {{agent_name}}, a deep research agent running in the FORGE ecosystem.\n\n" | |
| "Your primary duties:\n" | |
| "1. Execute research tasks rigorously using ReAct loops (max {{max_steps}} steps)\n" | |
| "2. Identify patterns across results - 3+ repeated insights = skill candidate\n" | |
| "3. When you notice a pattern, emit: SKILL_CANDIDATE: <description>\n" | |
| "4. After completing any task, perform a brief self-reflection:\n" | |
| " - What worked well?\n" | |
| " - What could be a reusable capability?\n" | |
| " - What slowed you down?\n\n" | |
| "ReAct discipline:\n" | |
| "- Think before acting. Write Thought: before every Action:\n" | |
| "- Cite sources. Never fabricate.\n" | |
| "- If uncertain, say so and use a tool to verify.\n\n" | |
| "You have access to FORGE. Use forge_client to load skills at runtime.\n" | |
| "Do not re-implement capabilities that already exist in FORGE." | |
| ) | |
| }), | |
| "schema_in": json.dumps({"agent_name": "str", "max_steps": "int"}), | |
| "schema_out": json.dumps({"rendered_prompt": "str"}), | |
| "deps": json.dumps([]), | |
| }, | |
| { | |
| "id": "task_decomposition_prompt", | |
| "version": "1.0.0", | |
| "type": "prompt", | |
| "name": "Task Decomposition Prompt", | |
| "description": "Prompt template for breaking a complex task into subtasks with dependency ordering. Outputs structured JSON.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["planning", "decomposition", "kanban"]), | |
| "payload": json.dumps({ | |
| "format": "plain", | |
| "variables": ["task", "context", "available_agents"], | |
| "template": ( | |
| "Decompose the following task into ordered subtasks.\n\n" | |
| "TASK: {{task}}\n" | |
| "CONTEXT: {{context}}\n" | |
| "AVAILABLE AGENTS: {{available_agents}}\n\n" | |
| "Output a JSON array where each item has:\n" | |
| " id: string (snake_case)\n" | |
| " title: string\n" | |
| " description: string\n" | |
| " agent: string (which agent should handle this)\n" | |
| " est_minutes: int\n" | |
| " deps: array of upstream task ids\n" | |
| " priority: 1-5 (5=critical)\n\n" | |
| "Respond ONLY with the JSON array. No markdown." | |
| ) | |
| }), | |
| "schema_in": json.dumps({"task": "str", "context": "str", "available_agents": "str"}), | |
| "schema_out": json.dumps({"subtasks": "array"}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- workflows --- | |
| { | |
| "id": "research_and_summarize", | |
| "version": "1.0.0", | |
| "type": "workflow", | |
| "name": "Research and Summarize", | |
| "description": "3-step workflow: web search, fetch top result, summarize. Outputs structured findings.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["research", "web", "summarize"]), | |
| "payload": json.dumps({ | |
| "entry": "search", | |
| "steps": [ | |
| {"id": "search", "cap_id": "web_search", "type": "skill", | |
| "params": {"query": "{{input.query}}", "max_results": 5}, | |
| "next": "fetch"}, | |
| {"id": "fetch", "cap_id": "http_fetch", "type": "skill", | |
| "params": {"url": "{{search.results[0].url}}", "max_chars": 3000}, | |
| "next": "summarize"}, | |
| {"id": "summarize", "cap_id": "text_summarizer", "type": "skill", | |
| "params": {"text": "{{fetch.content}}", "max_length": 150}, | |
| "next": None} | |
| ] | |
| }), | |
| "schema_in": json.dumps({"query": "str"}), | |
| "schema_out": json.dumps({"search_results": "list", "summary": "str"}), | |
| "deps": json.dumps(["web_search", "http_fetch", "text_summarizer"]), | |
| }, | |
| # --- knowledge --- | |
| { | |
| "id": "forge_api_reference", | |
| "version": "1.0.0", | |
| "type": "knowledge", | |
| "name": "FORGE API Reference", | |
| "description": "Complete REST API reference for FORGE v2. Inject this into any agent that needs to interact with the capability registry.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["forge", "api", "reference", "docs"]), | |
| "payload": json.dumps({ | |
| "format": "markdown", | |
| "source": BASE_URL, | |
| "content": ( | |
| "# FORGE v2 REST API\n\n" | |
| f"Base URL: {BASE_URL}\n\n" | |
| "## Capability Endpoints\n\n" | |
| "GET /api/capabilities - List/search capabilities\n" | |
| "GET /api/capabilities/{id} - Get capability (latest version)\n" | |
| "GET /api/capabilities/{id}/payload - Payload only (hot-load)\n" | |
| "GET /api/capabilities/{id}/resolve - Resolve bundle (all deps)\n" | |
| "POST /api/capabilities - Publish capability\n\n" | |
| "## MCP Endpoints\n\n" | |
| "GET /mcp/sse - SSE stream for MCP\n" | |
| "POST /mcp - JSON-RPC 2.0\n\n" | |
| "## MCP Tools: forge_search, forge_get, forge_publish, forge_list_types\n" | |
| ) | |
| }), | |
| "schema_in": json.dumps({}), | |
| "schema_out": json.dumps({"content": "str", "format": "str"}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- config --- | |
| { | |
| "id": "researcher_agent_config", | |
| "version": "1.0.0", | |
| "type": "config", | |
| "name": "Researcher Agent Config", | |
| "description": "Default configuration for the FORGE researcher agent. Controls timeouts, max steps, tool access, self-improvement thresholds.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["config", "researcher", "agent"]), | |
| "payload": json.dumps({ | |
| "settings": { | |
| "max_react_steps": 8, | |
| "llm_timeout_s": 120, | |
| "allowed_skills": ["web_search", "http_fetch", "text_summarizer", "calculator"], | |
| "self_improve": True, | |
| "pattern_threshold": 3, | |
| "skill_candidate_auto_draft": True, | |
| "memory_tiers": ["episodic", "semantic"], | |
| "trace_enabled": True | |
| } | |
| }), | |
| "schema_in": json.dumps({}), | |
| "schema_out": json.dumps({"settings": "dict"}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- mcp_ref --- | |
| { | |
| "id": "forge_mcp", | |
| "version": "1.0.0", | |
| "type": "mcp_ref", | |
| "name": "FORGE MCP Server", | |
| "description": "MCP server reference for FORGE itself. Connect Claude Desktop or any MCP client to the capability registry.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["mcp", "forge", "registry"]), | |
| "payload": json.dumps({ | |
| "url": f"{BASE_URL}/mcp/sse", | |
| "transport": "sse", | |
| "tools": ["forge_search", "forge_get", "forge_publish", "forge_list_types"], | |
| "npx_command": f"npx -y mcp-remote {BASE_URL}/mcp/sse", | |
| "auth": "none" | |
| }), | |
| "schema_in": json.dumps({}), | |
| "schema_out": json.dumps({}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- model_ref --- | |
| { | |
| "id": "qwen3_5_35b", | |
| "version": "1.0.0", | |
| "type": "model_ref", | |
| "name": "Qwen3.5-35B-A3B", | |
| "description": "ki-fusion RTX 5090 inference model. MoE architecture, 35B total / 3.5B active. Best for complex reasoning, code, multi-step tasks.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["llm", "qwen", "rtx5090", "ki-fusion", "moe"]), | |
| "payload": json.dumps({ | |
| "repo_id": "Qwen/Qwen3.5-35B-A3B", | |
| "provider": "ki-fusion-labs.de", | |
| "endpoint_env": "KI_FUSION_URL", | |
| "api_key_env": "KI_FUSION_KEY", | |
| "context_length": 32768, | |
| "strengths": ["reasoning", "code", "multi-step", "german"], | |
| "prompting_notes": "Supports thinking mode. Use <think> tags for chain-of-thought. Temperature 0.6 for creative, 0.1 for factual.", | |
| "compatible_with": ["openai_sdk", "lm_studio", "litellm"] | |
| }), | |
| "schema_in": json.dumps({}), | |
| "schema_out": json.dumps({}), | |
| "deps": json.dumps([]), | |
| }, | |
| # --- bundle --- | |
| { | |
| "id": "researcher_loadout", | |
| "version": "1.0.0", | |
| "type": "bundle", | |
| "name": "Researcher Agent Loadout", | |
| "description": "Everything a researcher agent needs: forge client, web search, HTTP fetch, summarizer, persona prompt, config, and model reference. One bundle, full capability.", | |
| "author": "Chris4K", | |
| "tags": json.dumps(["bundle", "researcher", "loadout", "starter"]), | |
| "payload": json.dumps({ | |
| "capabilities": [ | |
| "forge_client", | |
| "calculator", | |
| "researcher_persona", | |
| "task_decomposition_prompt", | |
| "researcher_agent_config", | |
| "forge_mcp", | |
| "qwen3_5_35b" | |
| ], | |
| "description": "Complete researcher agent capability set" | |
| }), | |
| "schema_in": json.dumps({}), | |
| "schema_out": json.dumps({}), | |
| "deps": json.dumps([ | |
| "forge_client", "calculator", "researcher_persona", | |
| "task_decomposition_prompt", "researcher_agent_config", | |
| "forge_mcp", "qwen3_5_35b" | |
| ]), | |
| }, | |
| ] | |
| for s in seeds: | |
| now = time.time() | |
| s.setdefault("schema_in", "{}") | |
| s.setdefault("schema_out", "{}") | |
| s.setdefault("deps", "[]") | |
| s.setdefault("meta", "{}") | |
| conn.execute(""" | |
| INSERT OR IGNORE INTO capabilities | |
| (id, version, type, name, description, author, tags, payload, | |
| schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at) | |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?) | |
| """, ( | |
| s["id"], s["version"], s["type"], s["name"], s["description"], | |
| s["author"], s["tags"], s["payload"], s["schema_in"], s["schema_out"], | |
| s["deps"], s.get("meta", "{}"), now, now | |
| )) | |
| conn.commit() | |
| conn.close() | |
| # --------------------------------------------------------------------------- | |
| # DB helpers | |
| # --------------------------------------------------------------------------- | |
| def _row_to_dict(row) -> dict: | |
| d = dict(row) | |
| for field in ("tags", "payload", "schema_in", "schema_out", "deps", "meta"): | |
| try: | |
| d[field] = json.loads(d.get(field) or "{}") | |
| except Exception: | |
| pass | |
| return d | |
| def db_list( | |
| q: str = "", | |
| cap_type: str = "", | |
| tag: str = "", | |
| limit: int = 50, | |
| offset: int = 0, | |
| ) -> list[dict]: | |
| conn = get_db() | |
| where, params = ["deprecated=0"], [] | |
| if cap_type and cap_type in VALID_TYPES: | |
| where.append("type=?"); params.append(cap_type) | |
| if tag: | |
| where.append("tags LIKE ?"); params.append(f'%"{tag}"%') | |
| if q: | |
| where.append("(name LIKE ? OR description LIKE ? OR tags LIKE ?)") | |
| params += [f"%{q}%", f"%{q}%", f"%{q}%"] | |
| sql = f""" | |
| SELECT id, version, type, name, description, author, tags, | |
| schema_in, schema_out, deps, downloads, created_at, updated_at | |
| FROM capabilities | |
| WHERE {' AND '.join(where)} | |
| GROUP BY id | |
| HAVING MAX(created_at) | |
| ORDER BY downloads DESC, created_at DESC | |
| LIMIT ? OFFSET ? | |
| """ | |
| rows = conn.execute(sql, params + [limit, offset]).fetchall() | |
| conn.close() | |
| return [_row_to_dict(r) for r in rows] | |
| def db_get(cap_id: str, version: str = "") -> Optional[dict]: | |
| conn = get_db() | |
| if version: | |
| row = conn.execute( | |
| "SELECT * FROM capabilities WHERE id=? AND version=?", (cap_id, version) | |
| ).fetchone() | |
| else: | |
| row = conn.execute( | |
| "SELECT * FROM capabilities WHERE id=? AND deprecated=0 ORDER BY created_at DESC LIMIT 1", | |
| (cap_id,) | |
| ).fetchone() | |
| conn.close() | |
| return _row_to_dict(row) if row else None | |
| def db_versions(cap_id: str) -> list[dict]: | |
| conn = get_db() | |
| rows = conn.execute( | |
| "SELECT id, version, name, author, downloads, deprecated, created_at FROM capabilities WHERE id=? ORDER BY created_at DESC", | |
| (cap_id,) | |
| ).fetchall() | |
| conn.close() | |
| return [_row_to_dict(r) for r in rows] | |
| def db_publish(cap: dict) -> tuple[bool, str]: | |
| cap_id = cap.get("id", "").strip() | |
| if not cap_id: | |
| return False, "Missing 'id'" | |
| cap_type = cap.get("type", "") | |
| if cap_type not in VALID_TYPES: | |
| return False, f"Invalid type '{cap_type}'. Valid: {sorted(VALID_TYPES)}" | |
| if not cap.get("name"): | |
| return False, "Missing 'name'" | |
| payload = cap.get("payload", {}) | |
| if not isinstance(payload, dict): | |
| return False, "'payload' must be a JSON object" | |
| # Type-specific validation | |
| if cap_type == "skill" and "code" not in payload: | |
| return False, "skill payload must contain 'code'" | |
| if cap_type == "prompt" and "template" not in payload: | |
| return False, "prompt payload must contain 'template'" | |
| if cap_type == "workflow" and "steps" not in payload: | |
| return False, "workflow payload must contain 'steps'" | |
| if cap_type == "bundle" and "capabilities" not in payload: | |
| return False, "bundle payload must contain 'capabilities'" | |
| if cap_type == "mcp_ref" and "url" not in payload: | |
| return False, "mcp_ref payload must contain 'url'" | |
| if cap_type == "model_ref" and "repo_id" not in payload: | |
| return False, "model_ref payload must contain 'repo_id'" | |
| now = time.time() | |
| version = cap.get("version", "1.0.0") | |
| conn = get_db() | |
| exists = conn.execute( | |
| "SELECT 1 FROM capabilities WHERE id=? AND version=?", (cap_id, version) | |
| ).fetchone() | |
| if exists: | |
| conn.close() | |
| return False, f"Capability '{cap_id}' v{version} already exists" | |
| conn.execute(""" | |
| INSERT INTO capabilities | |
| (id, version, type, name, description, author, tags, payload, | |
| schema_in, schema_out, deps, meta, downloads, deprecated, created_at, updated_at) | |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,0,?,?) | |
| """, ( | |
| cap_id, version, cap_type, | |
| cap.get("name", cap_id), | |
| cap.get("description", ""), | |
| cap.get("author", "anonymous"), | |
| json.dumps(cap.get("tags", [])), | |
| json.dumps(payload), | |
| json.dumps(cap.get("schema_in", {})), | |
| json.dumps(cap.get("schema_out", {})), | |
| json.dumps(cap.get("deps", [])), | |
| json.dumps(cap.get("meta", {})), | |
| now, now, | |
| )) | |
| conn.execute("INSERT INTO events (kind, cap_id, cap_type, meta, ts) VALUES (?,?,?,?,?)", | |
| ("publish", cap_id, cap_type, json.dumps({"version": version}), now)) | |
| conn.commit() | |
| conn.close() | |
| return True, f"Capability '{cap_id}' v{version} published" | |
| def db_stats() -> dict: | |
| conn = get_db() | |
| rows = conn.execute( | |
| "SELECT type, COUNT(*) as cnt, SUM(downloads) as dl FROM capabilities WHERE deprecated=0 GROUP BY type" | |
| ).fetchall() | |
| total = conn.execute("SELECT COUNT(*), SUM(downloads) FROM capabilities WHERE deprecated=0").fetchone() | |
| conn.close() | |
| by_type = {r["type"]: {"count": r["cnt"], "downloads": r["dl"] or 0} for r in rows} | |
| return { | |
| "total": total[0] or 0, | |
| "total_downloads": total[1] or 0, | |
| "by_type": by_type, | |
| } | |
| def db_resolve_bundle(bundle_id: str) -> Optional[dict]: | |
| cap = db_get(bundle_id) | |
| if not cap or cap["type"] != "bundle": | |
| return None | |
| cap_ids = cap["payload"].get("capabilities", []) | |
| resolved = {} | |
| for cid in cap_ids: | |
| c = db_get(cid) | |
| if c: | |
| resolved[cid] = c | |
| return {"bundle": cap, "resolved": resolved, "missing": [c for c in cap_ids if c not in resolved]} | |
| # --------------------------------------------------------------------------- | |
| # MCP Server | |
| # --------------------------------------------------------------------------- | |
| MCP_TOOLS = [ | |
| { | |
| "name": "forge_search", | |
| "description": "Search FORGE capability registry. Returns capabilities matching query, type, or tag filters.", | |
| "inputSchema": { | |
| "type": "object", | |
| "properties": { | |
| "query": {"type": "string", "description": "Search term"}, | |
| "type": {"type": "string", "description": "Filter by type (skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle)"}, | |
| "tag": {"type": "string", "description": "Filter by tag"}, | |
| "limit": {"type": "integer", "default": 10}, | |
| }, | |
| }, | |
| }, | |
| { | |
| "name": "forge_get", | |
| "description": "Get a specific capability from FORGE by ID. Returns full details including payload.", | |
| "inputSchema": { | |
| "type": "object", | |
| "required": ["id"], | |
| "properties": { | |
| "id": {"type": "string", "description": "Capability ID"}, | |
| "version": {"type": "string", "description": "Specific version (omit for latest)"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| "name": "forge_publish", | |
| "description": "Publish a new capability to FORGE registry.", | |
| "inputSchema": { | |
| "type": "object", | |
| "required": ["id", "type", "name", "payload"], | |
| "properties": { | |
| "id": {"type": "string"}, | |
| "type": {"type": "string", "description": "skill|prompt|workflow|knowledge|config|mcp_ref|model_ref|bundle"}, | |
| "name": {"type": "string"}, | |
| "version": {"type": "string", "default": "1.0.0"}, | |
| "description": {"type": "string"}, | |
| "author": {"type": "string"}, | |
| "tags": {"type": "array", "items": {"type": "string"}}, | |
| "payload": {"type": "object"}, | |
| "deps": {"type": "array", "items": {"type": "string"}}, | |
| }, | |
| }, | |
| }, | |
| { | |
| "name": "forge_list_types", | |
| "description": "List all capability types with counts and descriptions.", | |
| "inputSchema": {"type": "object", "properties": {}}, | |
| }, | |
| { | |
| "name": "forge_resolve_bundle", | |
| "description": "Resolve a bundle capability - returns all capabilities it contains.", | |
| "inputSchema": { | |
| "type": "object", | |
| "required": ["id"], | |
| "properties": {"id": {"type": "string", "description": "Bundle ID"}}, | |
| }, | |
| }, | |
| ] | |
| TYPE_DESCRIPTIONS = { | |
| "skill": "Executable Python code with def execute(). Hot-loadable at runtime.", | |
| "prompt": "System prompts, persona templates, jinja2/fstring templates.", | |
| "workflow": "Multi-step ReAct plans and orchestration graphs.", | |
| "knowledge": "Curated text/JSON chunks for RAG injection or agent context.", | |
| "config": "Agent behavior policies, guardrails, tool whitelists.", | |
| "mcp_ref": "Pointer to an external MCP server with connection details.", | |
| "model_ref": "HF model pointer with usage notes and prompting guide.", | |
| "bundle": "Named collection of capabilities (complete agent loadout).", | |
| } | |
| def handle_mcp(method: str, params: dict, req_id) -> dict: | |
| def ok(result): | |
| return {"jsonrpc": "2.0", "id": req_id, "result": result} | |
| if method == "initialize": | |
| return ok({ | |
| "protocolVersion": "2024-11-05", | |
| "serverInfo": {"name": "FORGE", "version": "2.0.0"}, | |
| "capabilities": {"tools": {}}, | |
| }) | |
| if method == "tools/list": | |
| return ok({"tools": MCP_TOOLS}) | |
| if method == "tools/call": | |
| name = params.get("name", "") | |
| args = params.get("arguments", {}) | |
| if name == "forge_search": | |
| caps = db_list( | |
| q=args.get("query", ""), | |
| cap_type=args.get("type", ""), | |
| tag=args.get("tag", ""), | |
| limit=args.get("limit", 10), | |
| ) | |
| # Strip payload for search results (may be large) | |
| for c in caps: | |
| c.pop("payload", None) | |
| return ok({"content": [{"type": "text", "text": json.dumps({"capabilities": caps, "count": len(caps)})}]}) | |
| if name == "forge_get": | |
| cap = db_get(args["id"], args.get("version", "")) | |
| if not cap: | |
| return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Not found: {args['id']}"})}]}) | |
| conn = get_db() | |
| conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?", | |
| (cap["id"], cap["version"])) | |
| conn.commit(); conn.close() | |
| return ok({"content": [{"type": "text", "text": json.dumps(cap)}]}) | |
| if name == "forge_publish": | |
| ok_flag, msg = db_publish(args) | |
| return ok({"content": [{"type": "text", "text": json.dumps({"ok": ok_flag, "message": msg})}]}) | |
| if name == "forge_list_types": | |
| stats = db_stats() | |
| types_info = [] | |
| for t in sorted(VALID_TYPES): | |
| info = stats["by_type"].get(t, {"count": 0, "downloads": 0}) | |
| types_info.append({ | |
| "type": t, | |
| "description": TYPE_DESCRIPTIONS[t], | |
| "count": info["count"], | |
| "downloads": info["downloads"], | |
| }) | |
| return ok({"content": [{"type": "text", "text": json.dumps({"types": types_info})}]}) | |
| if name == "forge_resolve_bundle": | |
| result = db_resolve_bundle(args["id"]) | |
| if not result: | |
| return ok({"content": [{"type": "text", "text": json.dumps({"error": f"Bundle not found: {args['id']}"})}]}) | |
| return ok({"content": [{"type": "text", "text": json.dumps(result)}]}) | |
| return {"jsonrpc": "2.0", "id": req_id, | |
| "error": {"code": -32601, "message": f"Unknown tool: {name}"}} | |
| if method in ("notifications/initialized", "notifications/cancelled"): | |
| return None | |
| return {"jsonrpc": "2.0", "id": req_id, | |
| "error": {"code": -32601, "message": f"Method not found: {method}"}} | |
| # --------------------------------------------------------------------------- | |
| # FastAPI app | |
| # --------------------------------------------------------------------------- | |
| async def lifespan(app: FastAPI): | |
| init_db() | |
| seed_db() | |
| yield | |
| app = FastAPI(title="FORGE v2", version="2.0.0", lifespan=lifespan) | |
| # --- REST API --------------------------------------------------------------- | |
| async def api_list( | |
| q: str = Query(""), | |
| type: str = Query(""), | |
| tag: str = Query(""), | |
| limit: int = Query(50, le=200), | |
| offset: int = Query(0), | |
| ): | |
| caps = db_list(q=q, cap_type=type, tag=tag, limit=limit, offset=offset) | |
| return JSONResponse({"capabilities": caps, "count": len(caps)}) | |
| async def api_versions(cap_id: str): | |
| versions = db_versions(cap_id) | |
| if not versions: | |
| raise HTTPException(404, f"Capability '{cap_id}' not found") | |
| return JSONResponse({"id": cap_id, "versions": versions}) | |
| async def api_resolve(cap_id: str): | |
| result = db_resolve_bundle(cap_id) | |
| if not result: | |
| raise HTTPException(404, f"Bundle '{cap_id}' not found or not a bundle") | |
| return JSONResponse(result) | |
| async def api_payload(cap_id: str, version: str = Query("")): | |
| cap = db_get(cap_id, version) | |
| if not cap: | |
| raise HTTPException(404, f"Capability '{cap_id}' not found") | |
| conn = get_db() | |
| conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?", | |
| (cap["id"], cap["version"])) | |
| conn.commit(); conn.close() | |
| return JSONResponse({"id": cap["id"], "version": cap["version"], | |
| "type": cap["type"], "payload": cap["payload"]}) | |
| async def api_get_version(cap_id: str, version: str): | |
| cap = db_get(cap_id, version) | |
| if not cap: | |
| raise HTTPException(404, f"Capability '{cap_id}' v{version} not found") | |
| return JSONResponse(cap) | |
| async def api_get(cap_id: str): | |
| cap = db_get(cap_id) | |
| if not cap: | |
| raise HTTPException(404, f"Capability '{cap_id}' not found") | |
| conn = get_db() | |
| conn.execute("UPDATE capabilities SET downloads=downloads+1 WHERE id=? AND version=?", | |
| (cap["id"], cap["version"])) | |
| conn.commit(); conn.close() | |
| return JSONResponse(cap) | |
| async def api_publish(request: Request): | |
| # Optional auth | |
| if FORGE_KEY: | |
| key = request.headers.get("x-forge-key", "") | |
| if key != FORGE_KEY: | |
| raise HTTPException(403, "Invalid X-Forge-Key") | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| raise HTTPException(400, "Invalid JSON") | |
| ok_flag, msg = db_publish(body) | |
| if not ok_flag: | |
| raise HTTPException(400, msg) | |
| return JSONResponse({"ok": True, "message": msg}) | |
| async def api_stats(): | |
| return JSONResponse(db_stats()) | |
| async def api_tags(): | |
| conn = get_db() | |
| rows = conn.execute("SELECT tags FROM capabilities WHERE deprecated=0").fetchall() | |
| conn.close() | |
| tags: set[str] = set() | |
| for r in rows: | |
| try: | |
| tags.update(json.loads(r["tags"])) | |
| except Exception: | |
| pass | |
| return JSONResponse({"tags": sorted(tags)}) | |
| # --------------------------------------------------------------------------- | |
| # Agent Config Handler — Sprint 5 | |
| # --------------------------------------------------------------------------- | |
| AGENT_DEFAULTS = { | |
| "heartbeat_seconds": 0, | |
| "cost_mode": "balanced", | |
| "max_react_steps": 6, | |
| "color": "#ff6b00", | |
| "tags": [], | |
| "enabled": True, | |
| } | |
| COST_MODES = ["cheap", "balanced", "best"] | |
| AGENT_COLORS = ["#ff6b00","#0ea5e9","#2ed573","#ff9500","#ff6b9d","#8b5cf6","#10b981","#f59e0b"] | |
| async def _push(url: str, path: str, payload: dict) -> dict: | |
| try: | |
| async with httpx.AsyncClient(timeout=8) as c: | |
| r = await c.post(url + path, json=payload) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception as e: | |
| return {"ok": False, "error": str(e)} | |
| async def _patch(url: str, path: str, payload: dict) -> dict: | |
| try: | |
| async with httpx.AsyncClient(timeout=8) as c: | |
| r = await c.patch(url + path, json=payload) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception as e: | |
| return {"ok": False, "error": str(e)} | |
| async def _get(url: str, path: str) -> dict | list | None: | |
| try: | |
| async with httpx.AsyncClient(timeout=6) as c: | |
| r = await c.get(url + path) | |
| r.raise_for_status() | |
| return r.json() | |
| except Exception: | |
| return None | |
| async def register_agent(request: Request): | |
| """ | |
| One-shot agent registration. | |
| Pushes to PULSE (agent config) AND agent-prompts (persona) simultaneously. | |
| Body: | |
| name str required — agent identifier (slug) | |
| persona str required — system prompt / persona description | |
| heartbeat_seconds int 0=manual only | |
| cost_mode str cheap | balanced | best | |
| max_react_steps int | |
| color str hex colour for UI | |
| tags list[str] | |
| enabled bool | |
| """ | |
| body = await request.json() | |
| name = (body.get("name") or "").strip().lower().replace(" ", "_") | |
| if not name: | |
| raise HTTPException(status_code=400, detail="name is required") | |
| persona = (body.get("persona") or "").strip() | |
| if not persona: | |
| raise HTTPException(status_code=400, detail="persona is required") | |
| agent_cfg = { | |
| "name": name, | |
| "persona": persona, | |
| "heartbeat_seconds": int(body.get("heartbeat_seconds", 0)), | |
| "cost_mode": body.get("cost_mode", "balanced"), | |
| "max_react_steps": int(body.get("max_react_steps", 6)), | |
| "color": body.get("color", "#ff6b00"), | |
| "tags": body.get("tags", []), | |
| "enabled": bool(body.get("enabled", True)), | |
| } | |
| results = {} | |
| # 1. Push agent config to PULSE | |
| pulse_r = await _push(PULSE_URL, "/api/agents", agent_cfg) | |
| results["pulse"] = pulse_r | |
| # 2. Push persona to agent-prompts | |
| prompts_payload = { | |
| "agent": name, | |
| "name": f"{name.upper()} Agent", | |
| "system_prompt": persona, | |
| "model_pref": agent_cfg["cost_mode"], | |
| "max_steps": agent_cfg["max_react_steps"], | |
| "tools": [], | |
| "config": {"color": agent_cfg["color"], "tags": agent_cfg["tags"]}, | |
| } | |
| prompts_r = await _push(PROMPTS_URL, "/api/personas", prompts_payload) | |
| results["prompts"] = prompts_r | |
| # 3. Also store as a FORGE config capability (self-registry) | |
| cap_id = f"agent_config_{name}" | |
| try: | |
| conn = get_db() | |
| now = int(time.time()) | |
| vid = str(uuid.uuid4())[:8] | |
| conn.execute(""" | |
| INSERT OR REPLACE INTO capabilities | |
| (id, name, description, type, payload, author, tags, version, | |
| created_at, updated_at, verified, download_count) | |
| VALUES (?,?,?,?,?,?,?,?,?,?,0,0) | |
| """, (cap_id, | |
| f"Agent Config: {name}", | |
| f"Registered agent configuration for {name}", | |
| "config", | |
| json.dumps(agent_cfg), | |
| "forge-config-handler", | |
| json.dumps(["agent", name, "config"]), | |
| vid, now, now)) | |
| conn.commit() | |
| results["forge_cap"] = cap_id | |
| except Exception as e: | |
| results["forge_cap"] = f"warning: {e}" | |
| pulse_ok = isinstance(pulse_r, dict) and "error" not in pulse_r | |
| prompts_ok = isinstance(prompts_r, dict) and "error" not in prompts_r | |
| all_ok = pulse_ok or prompts_ok # partial success is still useful | |
| return JSONResponse( | |
| status_code=201 if all_ok else 207, | |
| content={"ok": all_ok, "agent": name, "results": results} | |
| ) | |
| async def list_agents(): | |
| """Proxy PULSE /api/agents list. Falls back to FORGE config store.""" | |
| live = await _get(PULSE_URL, "/api/agents") | |
| if live is not None: | |
| return JSONResponse(live if isinstance(live, list) else [live]) | |
| # Fallback: pull from forge config store | |
| conn = get_db() | |
| rows = conn.execute( | |
| "SELECT payload FROM capabilities WHERE type='config' AND id LIKE 'agent_config_%'" | |
| ).fetchall() | |
| agents = [] | |
| for row in rows: | |
| try: agents.append(json.loads(row["payload"])) | |
| except Exception: pass | |
| return JSONResponse(agents) | |
| async def delete_agent(agent_name: str): | |
| """Disable an agent in PULSE.""" | |
| r = await _push(PULSE_URL, f"/api/agents/{agent_name}/disable", {}) | |
| return JSONResponse({"ok": True, "pulse": r}) | |
| async def trigger_agent(agent_name: str, request: Request): | |
| """Manually trigger an agent tick via PULSE.""" | |
| body = await request.json() | |
| r = await _push(PULSE_URL, f"/api/trigger/{agent_name}", body) | |
| return JSONResponse({"ok": True, "pulse": r}) | |
| async def api_skills_alias( | |
| q: str = Query(""), | |
| agent: str = Query(""), | |
| limit: int = Query(10, le=50), | |
| ): | |
| """Alias for /api/capabilities — used by PULSE agents to discover skills.""" | |
| # agent param: search capabilities tagged/named for this agent OR all | |
| search_q = q or agent | |
| caps = db_list(q=search_q, limit=limit) | |
| # Return in skills format expected by PULSE | |
| return JSONResponse({"skills": [ | |
| {"name": c["id"], "description": c.get("description","")[:120], | |
| "type": c.get("type",""), "tags": c.get("tags",[])} | |
| for c in caps | |
| ]}) | |
| async def health(): | |
| stats = db_stats() | |
| return JSONResponse({"ok": True, "capabilities": stats["total"], "version": "2.0.0"}) | |
| # --- MCP endpoints ---------------------------------------------------------- | |
| async def mcp_sse(request: Request): | |
| client_id = str(uuid.uuid4())[:8] | |
| async def event_gen(): | |
| yield f"data: {json.dumps({'jsonrpc':'2.0','method':'connected','params':{'client_id':client_id}})}\n\n" | |
| # Send available tools on connect | |
| tools_msg = { | |
| "jsonrpc": "2.0", "method": "notifications/tools", | |
| "params": {"tools": MCP_TOOLS} | |
| } | |
| yield f"data: {json.dumps(tools_msg)}\n\n" | |
| while True: | |
| if await request.is_disconnected(): | |
| break | |
| yield f": ping\n\n" | |
| await asyncio.sleep(15) | |
| return StreamingResponse( | |
| event_gen(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| ) | |
| async def mcp_jsonrpc(request: Request): | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| return JSONResponse({"jsonrpc": "2.0", "id": None, | |
| "error": {"code": -32700, "message": "Parse error"}}) | |
| # Batch support | |
| if isinstance(body, list): | |
| results = [handle_mcp(r.get("method", ""), r.get("params", {}), r.get("id")) for r in body] | |
| return JSONResponse([r for r in results if r is not None]) | |
| result = handle_mcp(body.get("method", ""), body.get("params", {}), body.get("id")) | |
| if result is None: | |
| return JSONResponse({"jsonrpc": "2.0", "id": body.get("id"), "result": {}}) | |
| return JSONResponse(result) | |
| # --------------------------------------------------------------------------- | |
| # SPA | |
| # --------------------------------------------------------------------------- | |
| SPA = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>⛢ FORGE v2 — Universal Capability Registry</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&family=DM+Mono:wght@300;400;500&display=swap'); | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#08080f;--surface:#0f0f1a;--surface2:#141421;--border:#1e1e30; | |
| --accent:#ff6b00;--accent2:#ff9500;--text:#e0e0f0;--muted:#5a5a7a; | |
| --green:#00ff88;--purple:#8b5cf6;--cyan:#06b6d4;--pink:#ec4899; | |
| --yellow:#f59e0b;--red:#ef4444;--indigo:#6366f1;--teal:#10b981; | |
| } | |
| html,body{height:100%;background:var(--bg);color:var(--text);font-family:'Syne',sans-serif} | |
| a{color:var(--accent);text-decoration:none} | |
| a:hover{text-decoration:underline} | |
| ::-webkit-scrollbar{width:6px;height:6px} | |
| ::-webkit-scrollbar-track{background:var(--surface)} | |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} | |
| /* Layout */ | |
| .app{display:flex;flex-direction:column;height:100vh} | |
| .header{padding:1rem 2rem;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:1.5rem} | |
| .logo{font-family:'Space Mono',monospace;font-size:1.6rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:-1px} | |
| .tagline{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase} | |
| .header-stats{margin-left:auto;display:flex;gap:1.5rem} | |
| .hstat{text-align:center} | |
| .hstat-num{font-family:'Space Mono',monospace;font-size:1.1rem;color:var(--accent);font-weight:700} | |
| .hstat-lbl{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em} | |
| .main{display:flex;flex:1;overflow:hidden} | |
| .sidebar{width:220px;min-width:220px;border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;overflow-y:auto} | |
| .content{flex:1;overflow-y:auto;padding:1.5rem} | |
| /* Sidebar */ | |
| .sidebar-section{padding:0.75rem 1rem 0.25rem;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);letter-spacing:0.2em;text-transform:uppercase} | |
| .type-btn{display:flex;align-items:center;gap:0.6rem;padding:0.55rem 1rem;cursor:pointer;font-size:0.82rem;transition:background 0.15s;border:none;background:none;color:var(--text);width:100%;text-align:left} | |
| .type-btn:hover{background:var(--surface2)} | |
| .type-btn.active{background:var(--surface2);border-left:3px solid var(--accent);color:var(--accent)} | |
| .type-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} | |
| .type-count{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted)} | |
| /* Tabs */ | |
| .tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:1.5rem;gap:0} | |
| .tab{padding:0.6rem 1.2rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);border-bottom:2px solid transparent;transition:all 0.15s;letter-spacing:0.05em} | |
| .tab.active{color:var(--accent);border-bottom-color:var(--accent)} | |
| .tab:hover{color:var(--text)} | |
| /* Cards */ | |
| .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem} | |
| .card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.1rem;cursor:pointer;transition:border-color 0.15s,transform 0.1s;position:relative;overflow:hidden} | |
| .card:hover{border-color:var(--accent);transform:translateY(-1px)} | |
| .card-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;margin-bottom:0.5rem;display:flex;align-items:center;gap:0.4rem} | |
| .card-name{font-family:'Space Mono',monospace;font-size:0.95rem;font-weight:700;color:var(--text);margin-bottom:0.3rem} | |
| .card-desc{font-size:0.8rem;color:var(--muted);line-height:1.5;margin-bottom:0.7rem} | |
| .card-footer{display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap} | |
| .tag{display:inline-block;background:#1a1a30;border:1px solid #2a2a50;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.6rem;padding:2px 7px;border-radius:20px;letter-spacing:0.05em} | |
| .card-meta{margin-left:auto;font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted)} | |
| /* Detail Panel */ | |
| .detail{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.5rem;margin-bottom:1.5rem} | |
| .detail-header{display:flex;align-items:flex-start;gap:1rem;margin-bottom:1rem} | |
| .detail-type{font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.15em;text-transform:uppercase;padding:3px 10px;border-radius:4px;border:1px solid} | |
| .detail-title{font-family:'Space Mono',monospace;font-size:1.4rem;font-weight:700;line-height:1.2} | |
| .detail-meta{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);margin-top:0.25rem} | |
| .detail-desc{font-size:0.88rem;line-height:1.7;color:var(--text);margin:0.75rem 0} | |
| .section-label{font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--purple);letter-spacing:0.15em;text-transform:uppercase;margin:1rem 0 0.5rem} | |
| pre{background:#0a0a14;border:1px solid var(--border);border-radius:6px;padding:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--green);overflow-x:auto;white-space:pre-wrap;line-height:1.6} | |
| .copy-btn{float:right;font-size:0.65rem;font-family:'DM Mono',monospace;background:var(--surface2);border:1px solid var(--border);color:var(--muted);padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.15s} | |
| .copy-btn:hover{color:var(--accent);border-color:var(--accent)} | |
| /* Search bar */ | |
| .search-row{display:flex;gap:0.75rem;margin-bottom:1.25rem;align-items:center} | |
| .search-input{flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s} | |
| .search-input:focus{border-color:var(--accent)} | |
| .search-input::placeholder{color:var(--muted)} | |
| .btn{padding:0.55rem 1.2rem;border:none;border-radius:6px;cursor:pointer;font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700;letter-spacing:0.05em;transition:all 0.15s} | |
| .btn-primary{background:var(--accent);color:#000} | |
| .btn-primary:hover{background:var(--accent2)} | |
| .btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border)} | |
| .btn-secondary:hover{border-color:var(--accent);color:var(--accent)} | |
| /* Publish form */ | |
| .form-group{margin-bottom:1rem} | |
| .form-label{display:block;font-family:'DM Mono',monospace;font-size:0.7rem;color:var(--muted);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:0.35rem} | |
| .form-input,.form-select,.form-textarea{width:100%;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:0.6rem 0.9rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.82rem;outline:none;transition:border-color 0.15s} | |
| .form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--accent)} | |
| .form-select option{background:var(--surface);color:var(--text)} | |
| .form-textarea{min-height:200px;resize:vertical;font-size:0.75rem} | |
| .form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem} | |
| .result-msg{padding:0.75rem 1rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:0.8rem;margin-top:0.75rem;display:none} | |
| .result-ok{background:#0a1f10;border:1px solid #1a4a20;color:var(--green)} | |
| .result-err{background:#1f0a0a;border:1px solid #4a1a1a;color:var(--red)} | |
| /* API docs */ | |
| .endpoint{background:var(--surface2);border-left:3px solid var(--accent);padding:0.65rem 1rem;margin:0.4rem 0;border-radius:0 6px 6px 0;font-family:'DM Mono',monospace;font-size:0.78rem} | |
| .method-get{color:var(--green)} | |
| .method-post{color:var(--accent2)} | |
| .method-delete{color:var(--red)} | |
| /* Bundle visualization */ | |
| .bundle-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.75rem;margin-top:0.75rem} | |
| .bundle-item{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;display:flex;align-items:center;gap:0.6rem} | |
| .bundle-item-icon{font-size:1.2rem} | |
| .bundle-item-name{font-family:'Space Mono',monospace;font-size:0.75rem;font-weight:700} | |
| .bundle-item-type{font-family:'DM Mono',monospace;font-size:0.6rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em} | |
| /* Misc */ | |
| .empty{text-align:center;padding:3rem;color:var(--muted);font-family:'DM Mono',monospace;font-size:0.85rem} | |
| .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-family:'DM Mono',monospace;font-size:0.65rem;letter-spacing:0.05em} | |
| .dep-chip{background:#1a1030;border:1px solid #2a2050;color:var(--purple);font-family:'DM Mono',monospace;font-size:0.65rem;padding:2px 8px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px} | |
| .dep-chip:hover{border-color:var(--purple);color:#a78bfa} | |
| .back-btn{display:inline-flex;align-items:center;gap:0.4rem;margin-bottom:1rem;font-family:'DM Mono',monospace;font-size:0.75rem;color:var(--muted);cursor:pointer} | |
| .back-btn:hover{color:var(--accent)} | |
| .version-list{display:flex;flex-direction:column;gap:0.5rem} | |
| .version-item{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0.75rem;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-family:'DM Mono',monospace;font-size:0.75rem} | |
| .version-tag{padding:2px 8px;background:#1a1a30;border:1px solid #2a2a50;border-radius:4px;color:var(--cyan)} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header class="header"> | |
| <div> | |
| <div class="logo">⛢ FORGE</div> | |
| <div class="tagline">Universal Capability Registry for AI Agents</div> | |
| </div> | |
| <div class="header-stats" id="headerStats"> | |
| <div class="hstat"><div class="hstat-num" id="statTotal">—</div><div class="hstat-lbl">Capabilities</div></div> | |
| <div class="hstat"><div class="hstat-num" id="statDownloads">—</div><div class="hstat-lbl">Downloads</div></div> | |
| <div class="hstat"><div class="hstat-num" id="statTypes">8</div><div class="hstat-lbl">Types</div></div> | |
| </div> | |
| </header> | |
| <div class="main"> | |
| <nav class="sidebar"> | |
| <div class="sidebar-section">Browse by Type</div> | |
| <button class="type-btn active" data-type="" onclick="filterType('')"> | |
| <span class="type-dot" style="background:#fff"></span> All | |
| <span class="type-count" id="cnt-all">—</span> | |
| </button> | |
| <button class="type-btn" data-type="skill" onclick="filterType('skill')"> | |
| <span class="type-dot" style="background:#ff6b00"></span> skill | |
| <span class="type-count" id="cnt-skill">0</span> | |
| </button> | |
| <button class="type-btn" data-type="prompt" onclick="filterType('prompt')"> | |
| <span class="type-dot" style="background:#8b5cf6"></span> prompt | |
| <span class="type-count" id="cnt-prompt">0</span> | |
| </button> | |
| <button class="type-btn" data-type="workflow" onclick="filterType('workflow')"> | |
| <span class="type-dot" style="background:#06b6d4"></span> workflow | |
| <span class="type-count" id="cnt-workflow">0</span> | |
| </button> | |
| <button class="type-btn" data-type="knowledge" onclick="filterType('knowledge')"> | |
| <span class="type-dot" style="background:#10b981"></span> knowledge | |
| <span class="type-count" id="cnt-knowledge">0</span> | |
| </button> | |
| <button class="type-btn" data-type="config" onclick="filterType('config')"> | |
| <span class="type-dot" style="background:#f59e0b"></span> config | |
| <span class="type-count" id="cnt-config">0</span> | |
| </button> | |
| <button class="type-btn" data-type="mcp_ref" onclick="filterType('mcp_ref')"> | |
| <span class="type-dot" style="background:#ef4444"></span> mcp_ref | |
| <span class="type-count" id="cnt-mcp_ref">0</span> | |
| </button> | |
| <button class="type-btn" data-type="model_ref" onclick="filterType('model_ref')"> | |
| <span class="type-dot" style="background:#ec4899"></span> model_ref | |
| <span class="type-count" id="cnt-model_ref">0</span> | |
| </button> | |
| <button class="type-btn" data-type="bundle" onclick="filterType('bundle')"> | |
| <span class="type-dot" style="background:#6366f1"></span> bundle | |
| <span class="type-count" id="cnt-bundle">0</span> | |
| </button> | |
| <div class="sidebar-section" style="margin-top:1rem">Navigation</div> | |
| <button class="type-btn" onclick="showTab('browse')">🔎 Browse</button> | |
| <button class="type-btn" onclick="showTab('publish')">📤 Publish</button> | |
| <button class="type-btn" onclick="showTab('api')">📡 API Docs</button> | |
| <button class="type-btn" onclick="showTab('agents')">🤖 Agents</button> | |
| </nav> | |
| <div class="content"> | |
| <!-- BROWSE TAB --> | |
| <div id="tab-browse"> | |
| <div id="view-list"> | |
| <div class="search-row"> | |
| <input class="search-input" id="searchInput" placeholder="Search capabilities..." oninput="debounceSearch()"> | |
| <button class="btn btn-primary" onclick="doSearch()">Search</button> | |
| </div> | |
| <div class="cards" id="cardGrid"></div> | |
| </div> | |
| <div id="view-detail" style="display:none"> | |
| <div class="back-btn" onclick="showList()">← Back to list</div> | |
| <div id="detailContent"></div> | |
| </div> | |
| </div> | |
| <!-- PUBLISH TAB --> | |
| <div id="tab-publish" style="display:none"> | |
| <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">Publish Capability</h2> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label class="form-label">ID *</label> | |
| <input class="form-input" id="pubId" placeholder="my_capability"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Version</label> | |
| <input class="form-input" id="pubVersion" value="1.0.0"> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label class="form-label">Type *</label> | |
| <select class="form-select" id="pubType" onchange="updatePayloadHint()"> | |
| <option value="skill">skill</option> | |
| <option value="prompt">prompt</option> | |
| <option value="workflow">workflow</option> | |
| <option value="knowledge">knowledge</option> | |
| <option value="config">config</option> | |
| <option value="mcp_ref">mcp_ref</option> | |
| <option value="model_ref">model_ref</option> | |
| <option value="bundle">bundle</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Author</label> | |
| <input class="form-input" id="pubAuthor" placeholder="your_name"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Name *</label> | |
| <input class="form-input" id="pubName" placeholder="Human-readable name"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Description</label> | |
| <input class="form-input" id="pubDesc" placeholder="What does this do?"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Tags (comma-separated)</label> | |
| <input class="form-input" id="pubTags" placeholder="utility, search, nlp"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Dependencies (comma-separated capability IDs)</label> | |
| <input class="form-input" id="pubDeps" placeholder="web_search, calculator"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Payload (JSON) *</label> | |
| <textarea class="form-textarea" id="pubPayload" style="min-height:250px;font-size:0.72rem"></textarea> | |
| <div style="font-family:'DM Mono',monospace;font-size:0.65rem;color:var(--muted);margin-top:0.3rem" id="payloadHint"></div> | |
| </div> | |
| <button class="btn btn-primary" onclick="publishCap()">⛢ Publish to FORGE</button> | |
| <div class="result-msg" id="pubResult"></div> | |
| </div> | |
| <!-- AGENTS TAB --> | |
| <div id="tab-agents" style="display:none"> | |
| <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">🤖 Agent Config Handler</h2> | |
| <p style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--muted);margin-bottom:1.5rem"> | |
| Register or update an agent in PULSE + agent-prompts in one shot. | |
| </p> | |
| <!-- Live agents grid --> | |
| <div style="margin-bottom:1.5rem"> | |
| <div style="display:flex;align-items:center;gap:1rem;margin-bottom:.75rem"> | |
| <span style="font-family:'DM Mono',monospace;font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.15em">Live Agents</span> | |
| <button class="btn btn-secondary" id="refreshAgentsBtn" style="padding:.25rem .75rem;font-size:.75rem">↻ Refresh</button> | |
| </div> | |
| <div id="agentGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.75rem"></div> | |
| </div> | |
| <!-- Registration form --> | |
| <div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:1.5rem;max-width:720px"> | |
| <h3 style="font-family:'Space Mono',monospace;font-size:.9rem;color:var(--accent);margin-bottom:1.25rem">+ Register / Update Agent</h3> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem"> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Agent Name (slug) *</label> | |
| <input id="ag-name" class="search-input" placeholder="researcher" style="width:100%"> | |
| </div> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Cost Mode</label> | |
| <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"> | |
| <option value="cheap">cheap (fast)</option> | |
| <option value="balanced" selected>balanced</option> | |
| <option value="best">best (slow)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem"> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Heartbeat (seconds, 0=manual)</label> | |
| <input id="ag-hb" type="number" min="0" value="0" class="search-input" style="width:100%"> | |
| </div> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Max ReAct Steps</label> | |
| <input id="ag-steps" type="number" min="1" max="20" value="6" class="search-input" style="width:100%"> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem"> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Tags (comma-separated)</label> | |
| <input id="ag-tags" class="search-input" placeholder="research,analysis" style="width:100%"> | |
| </div> | |
| <div> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">UI Color</label> | |
| <div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;margin-top:.2rem"> | |
| <input type="color" id="ag-color" value="#ff6b00" style="width:36px;height:36px;border:none;background:none;cursor:pointer;padding:0"> | |
| <span style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted)" id="ag-color-lbl">#ff6b00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="margin-bottom:1rem"> | |
| <label style="font-family:'DM Mono',monospace;font-size:.72rem;color:var(--muted);display:block;margin-bottom:.35rem">Persona / System Prompt *</label> | |
| <textarea id="ag-persona" class="search-input" rows="6" | |
| placeholder="You are a deep research specialist. Your job is to..." | |
| style="width:100%;resize:vertical;min-height:120px;font-family:'DM Mono',monospace;font-size:.8rem;line-height:1.5"></textarea> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem"> | |
| <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-family:'DM Mono',monospace;font-size:.78rem;color:var(--muted)"> | |
| <input type="checkbox" id="ag-enabled" checked style="accent-color:var(--accent)"> Enabled | |
| </label> | |
| </div> | |
| <div style="display:flex;gap:.75rem;align-items:center"> | |
| <button class="btn btn-primary" id="agRegisterBtn">🚀 Register Agent</button> | |
| <button class="btn btn-secondary" id="agClearBtn">Clear</button> | |
| <span id="agResult" style="font-family:'DM Mono',monospace;font-size:.78rem"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- API TAB --> | |
| <div id="tab-api" style="display:none"> | |
| <h2 style="font-family:'Space Mono',monospace;color:var(--accent);margin-bottom:1.25rem;font-size:1.1rem">FORGE API v2</h2> | |
| <p style="font-family:'DM Mono',monospace;font-size:0.8rem;color:var(--muted);margin-bottom:1.25rem"> | |
| Base: <code style="color:var(--accent2)" id="baseUrl"></code> | |
| </p> | |
| <div class="section-label">Capability Endpoints</div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities — <span style="color:var(--muted)">List/search. Params: q, type, tag, limit, offset</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id} — <span style="color:var(--muted)">Get capability (latest version)</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/{version} — <span style="color:var(--muted)">Get specific version</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/versions — <span style="color:var(--muted)">Version history</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/payload — <span style="color:var(--muted)">Payload only (for hot-loading)</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/capabilities/{id}/resolve — <span style="color:var(--muted)">Resolve bundle (all deps)</span></div> | |
| <div class="endpoint"><span class="method-post">POST</span> /api/capabilities — <span style="color:var(--muted)">Publish capability. Optional: X-Forge-Key header</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/stats — <span style="color:var(--muted)">Counts by type + downloads</span></div> | |
| <div class="endpoint"><span class="method-get">GET</span> /api/tags — <span style="color:var(--muted)">All tags</span></div> | |
| <div class="section-label" style="margin-top:1.5rem">MCP Server</div> | |
| <div class="endpoint"><span class="method-get">GET</span> /mcp/sse — <span style="color:var(--muted)">SSE stream (MCP transport)</span></div> | |
| <div class="endpoint"><span class="method-post">POST</span> /mcp — <span style="color:var(--muted)">JSON-RPC 2.0 endpoint</span></div> | |
| <div class="section-label" style="margin-top:1.5rem">MCP Tools</div> | |
| <div class="endpoint">forge_search(query, type, tag, limit)</div> | |
| <div class="endpoint">forge_get(id, version?)</div> | |
| <div class="endpoint">forge_publish({id, type, name, payload, ...})</div> | |
| <div class="endpoint">forge_list_types()</div> | |
| <div class="endpoint">forge_resolve_bundle(id)</div> | |
| <div class="section-label" style="margin-top:1.5rem">Quick Start</div> | |
| <div style="position:relative"> | |
| <button class="copy-btn" onclick="copyCode('quickstart')">Copy</button> | |
| <pre id="quickstart">import requests, types | |
| def bootstrap_forge(url=window.location.origin): | |
| r = requests.get(f"{url}/api/capabilities/forge_client/payload") | |
| m = types.ModuleType("forge_client") | |
| exec(r.json()["payload"]["code"], m.__dict__) | |
| return m.ForgeClient(url) | |
| forge = bootstrap_forge() | |
| # Load a skill | |
| calc = forge.load_skill("calculator") | |
| print(calc.execute(expression="sqrt(144) + 2**8")) | |
| # Get a prompt | |
| persona = forge.get_prompt("researcher_persona", | |
| variables={"agent_name": "MyAgent", "max_steps": "8"}) | |
| # Resolve a bundle (get everything) | |
| loadout = forge.get_bundle("researcher_loadout")</pre> | |
| </div> | |
| <div class="section-label" style="margin-top:1.5rem">Claude Desktop MCP Config</div> | |
| <div style="position:relative"> | |
| <button class="copy-btn" onclick="copyCode('mcpConfig')">Copy</button> | |
| <pre id="mcpConfig" style="color:var(--cyan)">{ | |
| "mcpServers": { | |
| "forge": { | |
| "command": "npx", | |
| "args": ["-y", "mcp-remote", "<span id='mcpUrl'></span>/mcp/sse"] | |
| } | |
| } | |
| }</pre> | |
| </div> | |
| <div class="section-label" style="margin-top:1.5rem">Capability Types Reference</div> | |
| <div id="typesTable" style="display:grid;grid-template-columns:100px 1fr;gap:0.4rem 1rem;font-family:'DM Mono',monospace;font-size:0.75rem;margin-top:0.5rem"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const TYPE_COLORS = { | |
| skill:'#ff6b00',prompt:'#8b5cf6',workflow:'#06b6d4',knowledge:'#10b981', | |
| config:'#f59e0b',mcp_ref:'#ef4444',model_ref:'#ec4899',bundle:'#6366f1' | |
| }; | |
| const TYPE_ICONS = { | |
| skill:'⚙',prompt:'💬',workflow:'🔗',knowledge:'📚', | |
| config:'⚙︎',mcp_ref:'📡',model_ref:'🤖',bundle:'📦' | |
| }; | |
| const TYPE_DESC = { | |
| skill:'Executable Python code (def execute(...)). Hot-loadable at runtime.', | |
| prompt:'System prompts, personas, Jinja2 / f-string templates.', | |
| workflow:'Multi-step ReAct plans and orchestration graphs.', | |
| knowledge:'Curated text/JSON chunks for RAG injection or agent context.', | |
| config:'Agent behavior policies, guardrails, tool whitelists.', | |
| mcp_ref:'Pointer to an external MCP server with connection details.', | |
| model_ref:'HF model pointer with usage notes and prompting guide.', | |
| bundle:'Named collection of capabilities (complete agent loadout).', | |
| }; | |
| let currentType = ''; | |
| let searchTimer = null; | |
| let currentCap = null; | |
| const BASE = window.location.origin; | |
| document.getElementById('baseUrl').textContent = BASE; | |
| document.querySelectorAll('#mcpUrl').forEach(el => el.textContent = BASE); | |
| // Types table | |
| const tt = document.getElementById('typesTable'); | |
| Object.entries(TYPE_DESC).forEach(([t,d]) => { | |
| tt.innerHTML += `<div style="color:${TYPE_COLORS[t]};font-weight:700">${t}</div><div style="color:var(--muted)">${d}</div>`; | |
| }); | |
| async function loadStats() { | |
| const r = await fetch('/api/stats'); const s = await r.json(); | |
| document.getElementById('statTotal').textContent = s.total; | |
| document.getElementById('statDownloads').textContent = s.total_downloads; | |
| document.getElementById('cnt-all').textContent = s.total; | |
| const bt = s.by_type || {}; | |
| ['skill','prompt','workflow','knowledge','config','mcp_ref','model_ref','bundle'].forEach(t => { | |
| const el = document.getElementById('cnt-'+t); | |
| if(el) el.textContent = bt[t]?.count || 0; | |
| }); | |
| } | |
| function filterType(t) { | |
| currentType = t; | |
| document.querySelectorAll('.type-btn').forEach(b => { | |
| b.classList.toggle('active', b.dataset.type === t); | |
| }); | |
| doSearch(); | |
| } | |
| function debounceSearch() { | |
| clearTimeout(searchTimer); | |
| searchTimer = setTimeout(doSearch, 300); | |
| } | |
| async function doSearch() { | |
| const q = document.getElementById('searchInput').value; | |
| const params = new URLSearchParams({q, limit:100}); | |
| if(currentType) params.set('type', currentType); | |
| const r = await fetch('/api/capabilities?' + params); | |
| const data = await r.json(); | |
| renderCards(data.capabilities || []); | |
| } | |
| function renderCards(caps) { | |
| const grid = document.getElementById('cardGrid'); | |
| if(!caps.length) { | |
| grid.innerHTML = '<div class="empty">No capabilities found.</div>'; | |
| return; | |
| } | |
| grid.innerHTML = caps.map(c => { | |
| const color = TYPE_COLORS[c.type] || '#fff'; | |
| const icon = TYPE_ICONS[c.type] || '?'; | |
| const tags = (c.tags || []).slice(0,4).map(t => `<span class="tag">${t}</span>`).join(''); | |
| return `<div class="card" onclick="showDetail('${c.id}')"> | |
| <div class="card-type" style="color:${color}">${icon} ${c.type}</div> | |
| <div class="card-name">${c.name}</div> | |
| <div class="card-desc">${(c.description||'').slice(0,120)}${c.description?.length>120?'…':''}</div> | |
| <div class="card-footer">${tags}<div class="card-meta">↓ ${c.downloads||0} · v${c.version}</div></div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| async function showDetail(id) { | |
| const r = await fetch(`/api/capabilities/${id}`); | |
| const cap = await r.json(); | |
| currentCap = cap; | |
| document.getElementById('view-list').style.display = 'none'; | |
| document.getElementById('view-detail').style.display = 'block'; | |
| renderDetail(cap); | |
| } | |
| function renderDetail(cap) { | |
| const color = TYPE_COLORS[cap.type] || '#fff'; | |
| const icon = TYPE_ICONS[cap.type] || ''; | |
| const tags = (cap.tags||[]).map(t=>`<span class="tag">${t}</span>`).join(''); | |
| const deps = (cap.deps||[]).map(d=>`<span class="dep-chip" onclick="showDetail('${d}')">${d}</span>`).join(''); | |
| let payloadHtml = ''; | |
| const p = cap.payload || {}; | |
| if(cap.type === 'skill') { | |
| payloadHtml = ` | |
| <div class="section-label">Code</div> | |
| <div style="position:relative"> | |
| <button class="copy-btn" onclick="copyText(decodeURIComponent('${encodeURIComponent(p.code||'')}'))">Copy</button> | |
| <pre>${escHtml(p.code||'')}</pre> | |
| </div> | |
| ${(p.dependencies||[]).length ? `<div class="section-label">Dependencies</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${p.dependencies.join(', ')}</div>` : ''} | |
| <div class="section-label">Quick Load</div> | |
| <pre style="color:var(--cyan)">forge = ForgeClient() | |
| skill = forge.load_skill("${cap.id}") | |
| result = skill.execute()</pre>`; | |
| } else if(cap.type === 'prompt') { | |
| payloadHtml = ` | |
| <div class="section-label">Template</div> | |
| <pre>${escHtml(p.template||'')}</pre> | |
| ${p.variables ? `<div class="section-label">Variables</div><div style="font-family:'DM Mono',monospace;font-size:0.78rem;color:var(--yellow)">${JSON.stringify(p.variables)}</div>` : ''} | |
| <div class="section-label">Quick Load</div> | |
| <pre style="color:var(--cyan)">prompt = forge.get_prompt("${cap.id}", variables={...})</pre>`; | |
| } else if(cap.type === 'workflow') { | |
| const steps = p.steps||[]; | |
| payloadHtml = ` | |
| <div class="section-label">Steps (${steps.length})</div> | |
| ${steps.map((s,i) => `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:0.75rem;margin-bottom:0.5rem;font-family:'DM Mono',monospace;font-size:0.75rem"> | |
| <span style="color:var(--cyan)">#${i+1} ${s.id}</span> → | |
| <span class="dep-chip" onclick="showDetail('${s.cap_id}')" style="cursor:pointer">${s.cap_id}</span> | |
| ${s.next ? `<span style="color:var(--muted)"> → ${s.next}</span>` : '<span style="color:var(--green)"> → END</span>'} | |
| </div>`).join('')}`; | |
| } else if(cap.type === 'bundle') { | |
| const caps_in = p.capabilities||[]; | |
| payloadHtml = ` | |
| <div class="section-label">Contains ${caps_in.length} Capabilities</div> | |
| <div class="bundle-grid">${caps_in.map(cid=>` | |
| <div class="bundle-item" onclick="showDetail('${cid}')" style="cursor:pointer"> | |
| <span class="bundle-item-icon">${TYPE_ICONS[cid]||'◯'}</span> | |
| <div><div class="bundle-item-name">${cid}</div></div> | |
| </div>`).join('')} | |
| </div> | |
| <div class="section-label" style="margin-top:1rem">Quick Load Bundle</div> | |
| <pre style="color:var(--cyan)">loadout = forge.get_bundle("${cap.id}") | |
| # Returns all ${caps_in.length} capabilities resolved</pre>`; | |
| } else { | |
| payloadHtml = ` | |
| <div class="section-label">Payload</div> | |
| <pre>${escHtml(JSON.stringify(p, null, 2))}</pre>`; | |
| } | |
| const schemaIn = JSON.stringify(cap.schema_in||{}, null, 2); | |
| const schemaOut = JSON.stringify(cap.schema_out||{}, null, 2); | |
| document.getElementById('detailContent').innerHTML = ` | |
| <div class="detail"> | |
| <div class="detail-header"> | |
| <div> | |
| <div class="detail-type" style="color:${color};border-color:${color}20;background:${color}10">${icon} ${cap.type}</div> | |
| </div> | |
| <div> | |
| <div class="detail-title">${cap.name}</div> | |
| <div class="detail-meta">v${cap.version} · by ${cap.author} · ↓ ${cap.downloads||0} downloads</div> | |
| </div> | |
| </div> | |
| <div class="detail-desc">${escHtml(cap.description||'')}</div> | |
| ${tags ? `<div style="margin-bottom:0.75rem">${tags}</div>` : ''} | |
| ${deps ? `<div class="section-label">Dependencies</div><div style="margin-bottom:0.75rem">${deps}</div>` : ''} | |
| ${payloadHtml} | |
| ${schemaIn !== '{}' ? `<div class="section-label">Input Schema</div><pre style="color:var(--purple)">${escHtml(schemaIn)}</pre>` : ''} | |
| ${schemaOut !== '{}' ? `<div class="section-label">Output Schema</div><pre style="color:var(--cyan)">${escHtml(schemaOut)}</pre>` : ''} | |
| </div>`; | |
| } | |
| function showList() { | |
| document.getElementById('view-list').style.display = 'block'; | |
| document.getElementById('view-detail').style.display = 'none'; | |
| currentCap = null; | |
| } | |
| function showTab(tab) { | |
| ['browse','publish','api','agents'].forEach(t => { | |
| document.getElementById('tab-'+t).style.display = t===tab?'block':'none'; | |
| }); | |
| if (tab === 'agents') loadAgents(); | |
| } | |
| // Publish | |
| const PAYLOAD_HINTS = { | |
| skill: '{"code": "def execute(x: str) -> dict:\\n return {\\"result\\": x}", "language": "python", "dependencies": []}', | |
| prompt: '{"template": "You are {{agent_name}}. {{instructions}}", "format": "plain", "variables": ["agent_name", "instructions"]}', | |
| workflow: '{"entry": "step1", "steps": [{"id": "step1", "cap_id": "some_skill", "type": "skill", "params": {}, "next": null}]}', | |
| knowledge: '{"format": "markdown", "source": "https://...", "content": "# Title\\n\\nContent here..."}', | |
| config: '{"settings": {"max_steps": 8, "timeout": 60, "allowed_tools": []}}', | |
| mcp_ref: '{"url": "https://example.com/mcp/sse", "transport": "sse", "tools": [], "auth": "none"}', | |
| model_ref: '{"repo_id": "org/model-name", "task": "text-generation", "notes": "Usage notes...", "context_length": 8192}', | |
| bundle: '{"capabilities": ["cap_id_1", "cap_id_2", "cap_id_3"], "description": "What this bundle provides"}', | |
| }; | |
| function updatePayloadHint() { | |
| const t = document.getElementById('pubType').value; | |
| const hint = PAYLOAD_HINTS[t] || '{}'; | |
| document.getElementById('pubPayload').value = hint; | |
| document.getElementById('payloadHint').textContent = `Required field for ${t}: ${ | |
| t==='skill'?'code' : t==='prompt'?'template' : t==='workflow'?'steps' : | |
| t==='bundle'?'capabilities' : t==='mcp_ref'?'url' : t==='model_ref'?'repo_id' : 'content/settings' | |
| }`; | |
| } | |
| updatePayloadHint(); | |
| async function publishCap() { | |
| const id = document.getElementById('pubId').value.trim(); | |
| const type = document.getElementById('pubType').value; | |
| const name = document.getElementById('pubName').value.trim(); | |
| const version = document.getElementById('pubVersion').value.trim() || '1.0.0'; | |
| const author = document.getElementById('pubAuthor').value.trim() || 'anonymous'; | |
| const desc = document.getElementById('pubDesc').value.trim(); | |
| const tagsRaw = document.getElementById('pubTags').value.trim(); | |
| const depsRaw = document.getElementById('pubDeps').value.trim(); | |
| const payloadRaw = document.getElementById('pubPayload').value.trim(); | |
| let payload; | |
| try { payload = JSON.parse(payloadRaw); } catch(e) { | |
| showResult('error', 'Invalid JSON payload: ' + e.message); return; | |
| } | |
| const body = { | |
| id, type, name, version, author, | |
| description: desc, | |
| tags: tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : [], | |
| deps: depsRaw ? depsRaw.split(',').map(d=>d.trim()).filter(Boolean) : [], | |
| payload, | |
| }; | |
| const r = await fetch('/api/capabilities', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(body), | |
| }); | |
| const data = await r.json(); | |
| if(r.ok) { | |
| showResult('ok', data.message); | |
| loadStats(); | |
| doSearch(); | |
| } else { | |
| showResult('error', data.detail || data.message || 'Error'); | |
| } | |
| } | |
| function showResult(type, msg) { | |
| const el = document.getElementById('pubResult'); | |
| el.className = 'result-msg ' + (type==='ok'?'result-ok':'result-err'); | |
| el.textContent = (type==='ok'?'✓ ':'✗ ') + msg; | |
| el.style.display = 'block'; | |
| setTimeout(()=>{ el.style.display='none'; }, 5000); | |
| } | |
| function copyCode(id) { | |
| const el = document.getElementById(id); | |
| navigator.clipboard.writeText(el.innerText || el.textContent); | |
| } | |
| function copyText(t) { navigator.clipboard.writeText(t); } | |
| function escHtml(s) { | |
| return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| } | |
| // Init | |
| loadStats(); | |
| // ── Agents tab ────────────────────────────────────────────────────── | |
| async function loadAgents() { | |
| const grid = document.getElementById('agentGrid'); | |
| grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">Loading…</span>'; | |
| try { | |
| const r = await fetch('/api/agents'); | |
| const agents = await r.json(); | |
| if (!agents || !agents.length) { | |
| grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--muted)">No agents registered yet.</span>'; | |
| return; | |
| } | |
| grid.innerHTML = agents.map(a => { | |
| const color = a.color || '#ff6b00'; | |
| const enabled = a.enabled !== false; | |
| return `<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ${color};border-radius:6px;padding:.85rem"> | |
| <div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem"> | |
| <span style="width:8px;height:8px;border-radius:50%;background:${enabled?'#00ff88':'#ef4444'};flex-shrink:0"></span> | |
| <span style="font-family:'Space Mono',monospace;font-size:.82rem;font-weight:700;color:${color}">${esc(a.name||'?')}</span> | |
| </div> | |
| <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> | |
| <div style="display:flex;gap:.5rem;flex-wrap:wrap"> | |
| <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> | |
| <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> | |
| ${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>`:''} | |
| </div> | |
| <div style="margin-top:.6rem;display:flex;gap:.4rem"> | |
| <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> | |
| <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> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| } catch(e) { | |
| grid.innerHTML = '<span style="font-family:'DM Mono',monospace;font-size:.8rem;color:var(--red)">Error loading agents: ' + esc(String(e)) + '</span>'; | |
| } | |
| } | |
| async function registerAgent() { | |
| const name = document.getElementById('ag-name').value.trim(); | |
| const persona = document.getElementById('ag-persona').value.trim(); | |
| if (!name || !persona) { | |
| setAgResult('error', 'Name and persona are required.'); | |
| return; | |
| } | |
| const tagsRaw = document.getElementById('ag-tags').value.trim(); | |
| const tags = tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : []; | |
| const payload = { | |
| name: name, | |
| persona: persona, | |
| heartbeat_seconds: parseInt(document.getElementById('ag-hb').value)||0, | |
| cost_mode: document.getElementById('ag-cost').value, | |
| max_react_steps: parseInt(document.getElementById('ag-steps').value)||6, | |
| color: document.getElementById('ag-color').value, | |
| tags: tags, | |
| enabled: document.getElementById('ag-enabled').checked, | |
| }; | |
| const btn = document.getElementById('agRegisterBtn'); | |
| btn.disabled = true; | |
| btn.textContent = 'Registering…'; | |
| setAgResult('muted','Pushing to PULSE + agent-prompts…'); | |
| try { | |
| const r = await fetch('/api/agents/register', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify(payload) | |
| }); | |
| const d = await r.json(); | |
| if (d.ok) { | |
| setAgResult('green', '✓ Agent “' + esc(name) + '” registered!'); | |
| clearAgentForm(); | |
| setTimeout(loadAgents, 800); | |
| } else { | |
| setAgResult('red', '✗ ' + JSON.stringify(d.results).slice(0,200)); | |
| } | |
| } catch(e) { | |
| setAgResult('red', 'Network error: ' + esc(String(e))); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '🚀 Register Agent'; | |
| } | |
| } | |
| async function triggerAgent(name) { | |
| try { | |
| const r = await fetch('/api/agents/' + encodeURIComponent(name) + '/trigger', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({content:'Manual trigger from FORGE UI', trigger_type:'manual'}) | |
| }); | |
| const d = await r.json(); | |
| setAgResult(d.ok?'green':'red', d.ok ? '▶ Triggered ' + esc(name) : 'Trigger failed: ' + JSON.stringify(d)); | |
| } catch(e) { setAgResult('red', String(e)); } | |
| } | |
| function prefillAgent(a) { | |
| document.getElementById('ag-name').value = a.name || ''; | |
| document.getElementById('ag-persona').value = a.persona || ''; | |
| document.getElementById('ag-hb').value = a.heartbeat_seconds || 0; | |
| document.getElementById('ag-cost').value = a.cost_mode || 'balanced'; | |
| document.getElementById('ag-steps').value = a.max_react_steps || 6; | |
| document.getElementById('ag-color').value = a.color || '#ff6b00'; | |
| document.getElementById('ag-color-lbl').textContent = a.color || '#ff6b00'; | |
| document.getElementById('ag-tags').value = (a.tags||[]).join(', '); | |
| document.getElementById('ag-enabled').checked = a.enabled !== false; | |
| document.getElementById('agRegisterBtn').scrollIntoView({behavior:'smooth',block:'nearest'}); | |
| } | |
| function clearAgentForm() { | |
| ['ag-name','ag-persona','ag-tags'].forEach(id => document.getElementById(id).value = ''); | |
| document.getElementById('ag-hb').value = '0'; | |
| document.getElementById('ag-cost').value = 'balanced'; | |
| document.getElementById('ag-steps').value = '6'; | |
| document.getElementById('ag-color').value = '#ff6b00'; | |
| document.getElementById('ag-color-lbl').textContent = '#ff6b00'; | |
| document.getElementById('ag-enabled').checked = true; | |
| setAgResult('',''); | |
| } | |
| function setAgResult(type, msg) { | |
| const el = document.getElementById('agResult'); | |
| const colors = {green:'var(--green)',red:'var(--red)',muted:'var(--muted)',error:'var(--red)','':`var(--text)`}; | |
| el.style.color = colors[type] || 'var(--text)'; | |
| el.innerHTML = msg; | |
| } | |
| // Wire up listeners once DOM ready | |
| document.addEventListener('DOMContentLoaded', () => { | |
| document.getElementById('agRegisterBtn').addEventListener('click', registerAgent); | |
| document.getElementById('agClearBtn').addEventListener('click', clearAgentForm); | |
| document.getElementById('refreshAgentsBtn').addEventListener('click', loadAgents); | |
| document.getElementById('ag-color').addEventListener('input', e => { | |
| document.getElementById('ag-color-lbl').textContent = e.target.value; | |
| }); | |
| }); | |
| doSearch(); | |
| </script> | |
| </body> | |
| </html>""" | |
| async def root(): | |
| return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8") | |
| # --------------------------------------------------------------------------- | |
| # Entry point | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") |