Spaces:
Sleeping
Sleeping
| """ | |
| agent-prompts — FORGE Prompt & Persona Registry | |
| Owns: prompt templates, personas, A/B variants, approval workflow, hot-serve API. | |
| Agents call GET /api/prompts/{id}/render at runtime — never hardcode system prompts again. | |
| """ | |
| import asyncio, json, os, sqlite3, time, uuid | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| import uvicorn | |
| from fastapi import FastAPI, HTTPException, Query, Request | |
| from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| DB_PATH = Path(os.getenv("PROMPTS_DB", "/tmp/prompts.db")) | |
| PORT = int(os.getenv("PORT", "7860")) | |
| PROMPTS_KEY = os.getenv("PROMPTS_KEY", "") # optional write auth | |
| VALID_TYPES = {"system","user","tool","chain","persona","fragment"} | |
| VALID_STATUSES = {"draft","approved","deprecated"} | |
| # --------------------------------------------------------------------------- | |
| # Database | |
| # --------------------------------------------------------------------------- | |
| def get_db(): | |
| conn = sqlite3.connect(str(DB_PATH), check_same_thread=False) | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| conn.execute("PRAGMA synchronous=NORMAL") | |
| return conn | |
| def init_db(): | |
| conn = get_db() | |
| conn.executescript(""" | |
| CREATE TABLE IF NOT EXISTS prompts ( | |
| id TEXT NOT NULL, | |
| version INTEGER NOT NULL DEFAULT 1, | |
| type TEXT NOT NULL DEFAULT 'system', | |
| agent TEXT NOT NULL DEFAULT '*', | |
| name TEXT NOT NULL, | |
| description TEXT NOT NULL DEFAULT '', | |
| template TEXT NOT NULL, | |
| variables TEXT NOT NULL DEFAULT '[]', | |
| tags TEXT NOT NULL DEFAULT '[]', | |
| status TEXT NOT NULL DEFAULT 'draft', | |
| ab_weight REAL NOT NULL DEFAULT 1.0, | |
| ab_group TEXT NOT NULL DEFAULT '', | |
| uses INTEGER NOT NULL DEFAULT 0, | |
| author TEXT NOT NULL DEFAULT 'Chris4K', | |
| created_at REAL NOT NULL, | |
| updated_at REAL NOT NULL, | |
| PRIMARY KEY (id, version) | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_pr_id ON prompts(id); | |
| CREATE INDEX IF NOT EXISTS idx_pr_agent ON prompts(agent); | |
| CREATE INDEX IF NOT EXISTS idx_pr_type ON prompts(type); | |
| CREATE INDEX IF NOT EXISTS idx_pr_status ON prompts(status); | |
| CREATE TABLE IF NOT EXISTS ab_results ( | |
| id TEXT PRIMARY KEY, | |
| prompt_id TEXT NOT NULL, | |
| version INTEGER NOT NULL, | |
| agent TEXT NOT NULL, | |
| session_id TEXT NOT NULL DEFAULT '', | |
| outcome TEXT NOT NULL DEFAULT 'unknown', | |
| latency_ms REAL, | |
| reward REAL, | |
| ts REAL NOT NULL | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_ab_pid ON ab_results(prompt_id); | |
| CREATE INDEX IF NOT EXISTS idx_ab_ts ON ab_results(ts DESC); | |
| CREATE TABLE IF NOT EXISTS personas ( | |
| id TEXT PRIMARY KEY, | |
| agent TEXT NOT NULL, | |
| name TEXT NOT NULL, | |
| system_prompt_id TEXT NOT NULL, | |
| model_pref TEXT NOT NULL DEFAULT '', | |
| max_steps INTEGER NOT NULL DEFAULT 8, | |
| tools TEXT NOT NULL DEFAULT '[]', | |
| config TEXT NOT NULL DEFAULT '{}', | |
| active INTEGER NOT NULL DEFAULT 1, | |
| created_at REAL NOT NULL, | |
| updated_at REAL NOT NULL | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_pe_agent ON personas(agent); | |
| """) | |
| conn.commit(); conn.close() | |
| # --------------------------------------------------------------------------- | |
| # Prompts CRUD | |
| # --------------------------------------------------------------------------- | |
| def prompt_list(agent="", ptype="", status="approved", tag="", q="", limit=50, offset=0): | |
| conn = get_db() | |
| where, params = [], [] | |
| if status: where.append("status=?"); params.append(status) | |
| if agent and agent != "*": | |
| where.append("(agent=? OR agent='*')"); params.append(agent) | |
| if ptype: where.append("type=?"); params.append(ptype) | |
| if tag: where.append("tags LIKE ?"); params.append(f'%"{tag}"%') | |
| if q: | |
| where.append("(name LIKE ? OR description LIKE ? OR template LIKE ?)") | |
| params += [f"%{q}%"]*3 | |
| sql = ("SELECT id,version,type,agent,name,description,variables,tags,status,ab_weight,ab_group,uses,author,created_at,updated_at " | |
| "FROM prompts" + (f" WHERE {' AND '.join(where)}" if where else "") + | |
| " GROUP BY id HAVING MAX(version) ORDER BY uses DESC, created_at DESC LIMIT ? OFFSET ?") | |
| rows = conn.execute(sql, params+[limit,offset]).fetchall() | |
| conn.close() | |
| return [_row(r) for r in rows] | |
| def prompt_get(pid: str, version: int = 0) -> dict | None: | |
| conn = get_db() | |
| if version: | |
| row = conn.execute("SELECT * FROM prompts WHERE id=? AND version=?", (pid, version)).fetchone() | |
| else: | |
| row = conn.execute("SELECT * FROM prompts WHERE id=? ORDER BY version DESC LIMIT 1", (pid,)).fetchone() | |
| conn.close() | |
| return _row(row) if row else None | |
| def prompt_versions(pid: str) -> list: | |
| conn = get_db() | |
| rows = conn.execute("SELECT id,version,name,status,uses,author,created_at FROM prompts WHERE id=? ORDER BY version DESC", (pid,)).fetchall() | |
| conn.close() | |
| return [_row(r) for r in rows] | |
| def prompt_create(data: dict) -> tuple[bool, str]: | |
| pid = str(data.get("id","")).strip() or str(uuid.uuid4())[:12] | |
| name = str(data.get("name","")).strip() | |
| tmpl = str(data.get("template","")).strip() | |
| if not name: return False, "Missing 'name'" | |
| if not tmpl: return False, "Missing 'template'" | |
| ptype = str(data.get("type","system")) | |
| if ptype not in VALID_TYPES: ptype = "system" | |
| status = str(data.get("status","draft")) | |
| if status not in VALID_STATUSES: status = "draft" | |
| now = time.time() | |
| conn = get_db() | |
| cur_v = conn.execute("SELECT MAX(version) FROM prompts WHERE id=?", (pid,)).fetchone()[0] or 0 | |
| new_v = cur_v + 1 | |
| conn.execute(""" | |
| INSERT INTO prompts (id,version,type,agent,name,description,template,variables,tags, | |
| status,ab_weight,ab_group,uses,author,created_at,updated_at) | |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,?,?,?)""", | |
| (pid, new_v, ptype, | |
| str(data.get("agent","*")), | |
| name, str(data.get("description","")), tmpl, | |
| json.dumps(data.get("variables",[])), | |
| json.dumps(data.get("tags",[])), | |
| status, | |
| float(data.get("ab_weight",1.0)), | |
| str(data.get("ab_group","")), | |
| str(data.get("author","anonymous")), | |
| now, now)) | |
| conn.commit(); conn.close() | |
| return True, f"Prompt '{pid}' v{new_v} created" | |
| def prompt_approve(pid: str, version: int = 0) -> bool: | |
| conn = get_db() | |
| if version: | |
| n = conn.execute("UPDATE prompts SET status='approved', updated_at=? WHERE id=? AND version=?", | |
| (time.time(), pid, version)).rowcount | |
| else: | |
| n = conn.execute("UPDATE prompts SET status='approved', updated_at=? WHERE id=? AND version=(SELECT MAX(version) FROM prompts WHERE id=?)", | |
| (time.time(), pid, pid)).rowcount | |
| conn.commit(); conn.close() | |
| return n > 0 | |
| def prompt_deprecate(pid: str) -> bool: | |
| conn = get_db() | |
| n = conn.execute("UPDATE prompts SET status='deprecated', updated_at=? WHERE id=?", | |
| (time.time(), pid)).rowcount | |
| conn.commit(); conn.close() | |
| return n > 0 | |
| def _row(row) -> dict: | |
| if not row: return {} | |
| d = dict(row) | |
| for f in ("variables","tags","config","tools"): | |
| if f in d: | |
| try: d[f] = json.loads(d[f]) | |
| except Exception: pass | |
| return d | |
| # --------------------------------------------------------------------------- | |
| # Render engine | |
| # --------------------------------------------------------------------------- | |
| def render_template(template: str, variables: dict) -> str: | |
| """Simple {{var}} substitution — no deps, no security issues.""" | |
| result = template | |
| for k, v in (variables or {}).items(): | |
| result = result.replace("{{" + k + "}}", str(v)) | |
| result = result.replace("{{ " + k + " }}", str(v)) | |
| return result | |
| def prompt_render(pid: str, variables: dict = None, version: int = 0, | |
| ab_select: bool = True) -> dict: | |
| """ | |
| Render a prompt with variables substituted. | |
| If ab_select=True and multiple approved versions exist, picks by weight. | |
| """ | |
| conn = get_db() | |
| if ab_select and not version: | |
| # Get all approved versions of this prompt for A/B selection | |
| rows = conn.execute( | |
| "SELECT * FROM prompts WHERE id=? AND status='approved' ORDER BY version DESC", | |
| (pid,)).fetchall() | |
| conn.close() | |
| if not rows: | |
| return {"error": f"Prompt '{pid}' not found or not approved"} | |
| import random | |
| weights = [r["ab_weight"] for r in rows] | |
| total = sum(weights) | |
| norm = [w/total for w in weights] | |
| chosen = random.choices(rows, weights=norm, k=1)[0] | |
| prompt = _row(chosen) | |
| else: | |
| conn.close() | |
| prompt = prompt_get(pid, version) | |
| if not prompt: | |
| return {"error": f"Prompt '{pid}' not found"} | |
| rendered = render_template(prompt["template"], variables or {}) | |
| # Increment use counter | |
| conn2 = get_db() | |
| conn2.execute("UPDATE prompts SET uses=uses+1 WHERE id=? AND version=?", | |
| (prompt["id"], prompt["version"])) | |
| conn2.commit(); conn2.close() | |
| return { | |
| "id": prompt["id"], | |
| "version": prompt["version"], | |
| "type": prompt["type"], | |
| "agent": prompt["agent"], | |
| "rendered": rendered, | |
| "variables_used": list((variables or {}).keys()), | |
| "ab_group": prompt.get("ab_group",""), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Personas | |
| # --------------------------------------------------------------------------- | |
| def persona_get(agent: str) -> dict | None: | |
| conn = get_db() | |
| row = conn.execute("SELECT * FROM personas WHERE agent=? AND active=1 ORDER BY updated_at DESC LIMIT 1", (agent,)).fetchone() | |
| conn.close() | |
| if not row: return None | |
| p = _row(row) | |
| # Embed the actual rendered system prompt | |
| sp = prompt_render(p["system_prompt_id"]) | |
| p["system_prompt"] = sp.get("rendered","") | |
| return p | |
| def persona_upsert(data: dict) -> str: | |
| agent = str(data.get("agent","")).strip() | |
| if not agent: raise ValueError("Missing 'agent'") | |
| pid = str(data.get("system_prompt_id","")).strip() | |
| if not pid: raise ValueError("Missing 'system_prompt_id'") | |
| now = time.time() | |
| persona_id = str(uuid.uuid4()) | |
| conn = get_db() | |
| # Deactivate old | |
| conn.execute("UPDATE personas SET active=0 WHERE agent=?", (agent,)) | |
| conn.execute(""" | |
| INSERT INTO personas (id,agent,name,system_prompt_id,model_pref,max_steps,tools,config,active,created_at,updated_at) | |
| VALUES (?,?,?,?,?,?,?,?,1,?,?)""", | |
| (persona_id, agent, | |
| str(data.get("name", f"{agent} persona")), | |
| pid, | |
| str(data.get("model_pref","")), | |
| int(data.get("max_steps",8)), | |
| json.dumps(data.get("tools",[])), | |
| json.dumps(data.get("config",{})), | |
| now, now)) | |
| conn.commit(); conn.close() | |
| return persona_id | |
| def persona_list() -> list: | |
| conn = get_db() | |
| rows = conn.execute("SELECT * FROM personas WHERE active=1 ORDER BY updated_at DESC").fetchall() | |
| conn.close() | |
| return [_row(r) for r in rows] | |
| # --------------------------------------------------------------------------- | |
| # A/B result recording | |
| # --------------------------------------------------------------------------- | |
| def ab_record(prompt_id: str, version: int, agent: str, | |
| session_id: str, outcome: str, latency_ms: float = None, reward: float = None): | |
| conn = get_db() | |
| conn.execute(""" | |
| INSERT INTO ab_results (id,prompt_id,version,agent,session_id,outcome,latency_ms,reward,ts) | |
| VALUES (?,?,?,?,?,?,?,?,?)""", | |
| (str(uuid.uuid4()), prompt_id, version, agent, | |
| session_id, outcome, latency_ms, reward, time.time())) | |
| conn.commit(); conn.close() | |
| def ab_stats(prompt_id: str) -> dict: | |
| conn = get_db() | |
| rows = conn.execute(""" | |
| SELECT version, COUNT(*) as n, | |
| AVG(CASE WHEN outcome='success' THEN 1.0 ELSE 0.0 END) as success_rate, | |
| AVG(reward) as avg_reward, AVG(latency_ms) as avg_lat | |
| FROM ab_results WHERE prompt_id=? | |
| GROUP BY version ORDER BY version""", (prompt_id,)).fetchall() | |
| conn.close() | |
| return {"prompt_id": prompt_id, "versions": [dict(r) for r in rows]} | |
| # --------------------------------------------------------------------------- | |
| # Stats | |
| # --------------------------------------------------------------------------- | |
| def get_stats() -> dict: | |
| conn = get_db() | |
| total = conn.execute("SELECT COUNT(DISTINCT id) FROM prompts").fetchone()[0] | |
| by_type = conn.execute("SELECT type, COUNT(DISTINCT id) as n FROM prompts WHERE status='approved' GROUP BY type").fetchall() | |
| by_stat = conn.execute("SELECT status, COUNT(*) as n FROM prompts GROUP BY status").fetchall() | |
| top = conn.execute("SELECT id,name,uses FROM prompts GROUP BY id HAVING MAX(version) ORDER BY uses DESC LIMIT 8").fetchall() | |
| n_ab = conn.execute("SELECT COUNT(*) FROM ab_results").fetchone()[0] | |
| n_per = conn.execute("SELECT COUNT(*) FROM personas WHERE active=1").fetchone()[0] | |
| conn.close() | |
| return { | |
| "total_prompts": total, | |
| "active_personas": n_per, | |
| "ab_results": n_ab, | |
| "by_type": {r["type"]: r["n"] for r in by_type}, | |
| "by_status": {r["status"]: r["n"] for r in by_stat}, | |
| "top_used": [dict(r) for r in top], | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Seed: migrate all hardcoded prompts from NEXUS, PULSE, MEMORY | |
| # --------------------------------------------------------------------------- | |
| def seed_prompts(): | |
| conn = get_db() | |
| if conn.execute("SELECT COUNT(*) FROM prompts").fetchone()[0] > 0: | |
| conn.close(); return | |
| conn.close() | |
| seeds = [ | |
| # NEXUS system prompt | |
| { | |
| "id": "nexus_router", | |
| "type": "system", | |
| "agent": "nexus", | |
| "name": "NEXUS Router Persona", | |
| "description": "Core system prompt for the NEXUS LLM routing agent. Controls routing logic, slot management, and fallback behavior.", | |
| "template": ( | |
| "You are NEXUS, the intelligent LLM routing hub for the FORGE AI ecosystem.\n\n" | |
| "Your responsibilities:\n" | |
| "1. Route every inference request to the best available provider based on Q-table guidance from agent-learn\n" | |
| "2. Manage LLM slots — reserve before long tasks, release on completion\n" | |
| "3. Log every inference to agent-trace (fire-and-forget, never block on it)\n" | |
| "4. Fall back gracefully: ki-fusion RTX5090 → Anthropic claude-haiku → HF API → local_cpu\n\n" | |
| "Routing principles:\n" | |
| "- Prefer ki-fusion for reasoning, code, German language tasks\n" | |
| "- Prefer HF API for simple classification and embeddings\n" | |
| "- Never route to local_cpu unless all else fails\n" | |
| "- If a provider fails twice in 60s, mark it unhealthy\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": [], | |
| "tags": ["nexus","routing","system","core"], | |
| "status": "approved", | |
| }, | |
| # PULSE scheduler persona | |
| { | |
| "id": "pulse_scheduler", | |
| "type": "system", | |
| "agent": "pulse", | |
| "name": "PULSE Scheduler Persona", | |
| "description": "System prompt for the PULSE scheduling and ReAct agent. Controls task execution, heartbeat, and delegation behavior.", | |
| "template": ( | |
| "You are PULSE, the scheduling and orchestration agent for the FORGE AI ecosystem.\n\n" | |
| "Your responsibilities:\n" | |
| "1. Execute scheduled tasks using ReAct loops (max {{max_steps}} steps)\n" | |
| "2. Reserve LLM slots before long-running inference tasks\n" | |
| "3. Delegate subtasks to specialized agents via trigger_agent\n" | |
| "4. Track all tasks in KANBAN — move cards through todo→in_progress→done\n" | |
| "5. Log every action to agent-trace\n\n" | |
| "ReAct discipline:\n" | |
| "- Thought: before every Action:\n" | |
| "- Observation: after every tool call\n" | |
| "- Never hallucinate tool results\n" | |
| "- If a tool fails, try once more then mark task failed\n\n" | |
| "Self-improvement:\n" | |
| "- After completing any task, call self_reflect\n" | |
| "- If you notice a pattern repeated 3+ times, call learn_candidate_add\n" | |
| "- Load skills from FORGE rather than re-implementing capabilities\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["max_steps"], | |
| "tags": ["pulse","scheduler","react","system","core"], | |
| "status": "approved", | |
| }, | |
| # Self-reflection prompt (migrated out of PULSE) | |
| { | |
| "id": "self_reflect_prompt", | |
| "type": "user", | |
| "agent": "*", | |
| "name": "Self-Reflection Prompt", | |
| "description": "Prompt injected after task completion to trigger agent self-reflection. Outputs structured improvement proposals.", | |
| "template": ( | |
| "You just completed the task: {{task_description}}\n" | |
| "Result status: {{status}}\n" | |
| "Steps taken: {{steps_taken}}\n\n" | |
| "Reflect on this execution:\n" | |
| "1. What worked well?\n" | |
| "2. What was inefficient or could be improved?\n" | |
| "3. Did you repeat any pattern that should become a reusable skill?\n" | |
| "4. What would you do differently next time?\n\n" | |
| "Output a JSON object with keys: 'worked_well', 'improvements', 'skill_candidate' (null or description string), 'next_time'.\n" | |
| "Respond ONLY with the JSON object." | |
| ), | |
| "variables": ["task_description","status","steps_taken"], | |
| "tags": ["self-improvement","reflection","all-agents"], | |
| "status": "approved", | |
| }, | |
| # Task decomposition prompt | |
| { | |
| "id": "task_decompose", | |
| "type": "user", | |
| "agent": "*", | |
| "name": "Task Decomposition", | |
| "description": "Break a complex task into ordered subtasks with agent assignments, estimates, and dependencies.", | |
| "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\n" | |
| " est_minutes: int\n" | |
| " deps: array of upstream task ids\n" | |
| " priority: 1-5\n\n" | |
| "Respond ONLY with the JSON array. No markdown." | |
| ), | |
| "variables": ["task","context","available_agents"], | |
| "tags": ["planning","kanban","decomposition"], | |
| "status": "approved", | |
| }, | |
| # Error recovery prompt | |
| { | |
| "id": "error_recovery", | |
| "type": "user", | |
| "agent": "*", | |
| "name": "Error Recovery Prompt", | |
| "description": "Prompt for handling tool/API errors gracefully in ReAct loops.", | |
| "template": ( | |
| "The following error occurred: {{error_message}}\n" | |
| "Tool: {{tool_name}}\n" | |
| "Attempt: {{attempt_number}} of {{max_attempts}}\n\n" | |
| "Assess the error:\n" | |
| "1. Is this retryable? (network timeout, rate limit = yes; auth error, not found = no)\n" | |
| "2. Is there an alternative approach?\n" | |
| "3. Should the task be marked failed and escalated?\n\n" | |
| "Respond with JSON: {\"retryable\": bool, \"action\": \"retry\"|\"alternative\"|\"fail\", \"reason\": string, \"alternative_tool\": string|null}" | |
| ), | |
| "variables": ["error_message","tool_name","attempt_number","max_attempts"], | |
| "tags": ["error-handling","react","resilience"], | |
| "status": "approved", | |
| }, | |
| # ReAct system template | |
| { | |
| "id": "react_base", | |
| "type": "system", | |
| "agent": "*", | |
| "name": "ReAct Base System Prompt", | |
| "description": "Generic ReAct system prompt fragment. Inject into any agent to enforce Thought/Action/Observation discipline.", | |
| "template": ( | |
| "You operate using the ReAct (Reason + Act) framework:\n\n" | |
| "Format every response as:\n" | |
| "Thought: <your reasoning before acting>\n" | |
| "Action: <tool_name>({\"param\": \"value\"})\n" | |
| "Observation: <tool result>\n" | |
| "... repeat ...\n" | |
| "Final Answer: <your conclusion>\n\n" | |
| "Rules:\n" | |
| "- Maximum {{max_steps}} reasoning steps\n" | |
| "- Never fabricate tool results\n" | |
| "- Cite your reasoning before every action\n" | |
| "- If uncertain, use a tool to verify rather than guessing" | |
| ), | |
| "variables": ["max_steps"], | |
| "tags": ["react","fragment","base"], | |
| "status": "approved", | |
| }, | |
| # RELAY notification prompt | |
| { | |
| "id": "relay_notification", | |
| "type": "user", | |
| "agent": "relay", | |
| "name": "Agent Notification Template", | |
| "description": "Template for RELAY to format agent notifications to Telegram/webhooks.", | |
| "template": ( | |
| "☀️ *FORGE {{agent}}*\n" | |
| "📅 {{timestamp}}\n\n" | |
| "{{message}}\n\n" | |
| "{% if details %}_Details:_ {{details}}{% endif %}" | |
| ), | |
| "variables": ["agent","timestamp","message","details"], | |
| "tags": ["relay","notification","telegram"], | |
| "status": "approved", | |
| }, | |
| # LOOP improvement cycle prompt | |
| { | |
| "id": "loop_improvement_cycle", | |
| "type": "system", | |
| "agent": "loop", | |
| "name": "LOOP Improvement Cycle Prompt", | |
| "description": "System prompt for the agent-loop improvement orchestrator. Controls the trace→learn→prompts→deploy cycle.", | |
| "template": ( | |
| "You are LOOP, the self-improvement orchestrator for the FORGE AI ecosystem.\n\n" | |
| "Your improvement cycle runs every {{cycle_interval_minutes}} minutes:\n" | |
| "1. Pull reward trend from agent-learn — identify agents with avg_reward < {{threshold}}\n" | |
| "2. For underperforming agents, propose prompt improvements using their self-reflection data\n" | |
| "3. Create draft prompt versions in agent-prompts (status=draft)\n" | |
| "4. Notify {{notify_agent}} (via RELAY) for approval\n" | |
| "5. On approval: update persona in agent-prompts, trigger agent restart via PULSE\n" | |
| "6. After 24h: compare before/after reward trends, log outcome to agent-trace\n\n" | |
| "Decision thresholds:\n" | |
| "- avg_reward < {{threshold}}: trigger improvement cycle\n" | |
| "- error_rate > 15%: escalate immediately to {{notify_agent}}\n" | |
| "- improvement delta < 0.05 after 24h: mark cycle as inconclusive\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["cycle_interval_minutes","threshold","notify_agent"], | |
| "tags": ["loop","self-improvement","orchestrator","system"], | |
| "status": "approved", | |
| }, | |
| # ── Sub-agent personas (Sprint 4) ─────────────────────────── | |
| # RESEARCHER | |
| { | |
| "id": "researcher_persona", | |
| "type": "system", | |
| "agent": "researcher", | |
| "name": "RESEARCHER Sub-Agent Persona", | |
| "description": "System prompt for the researcher sub-agent. Deep search, analysis, citation, and synthesis specialist.", | |
| "template": ( | |
| "You are RESEARCHER, a specialist sub-agent inside the FORGE AI ecosystem at ki-fusion-labs.de.\n\n" | |
| "Your strengths:\n" | |
| "- Deep analysis of technical documents, papers, and structured data\n" | |
| "- Web and knowledge-base search (agent-knowledge RAG store)\n" | |
| "- Fact-checking, citation, and source verification\n" | |
| "- Synthesis of findings into structured summaries\n\n" | |
| "Working style:\n" | |
| "- Always cite your sources and tag confidence levels (HIGH/MED/LOW)\n" | |
| "- Prefer primary sources over aggregators\n" | |
| "- When uncertain, say so — never hallucinate facts\n" | |
| "- Deliver findings as structured JSON when asked by PULSE\n\n" | |
| "Scope constraints:\n" | |
| "- You do NOT write or execute code — delegate to CODER\n" | |
| "- You do NOT manage tasks — coordinate via PULSE\n" | |
| "- Maximum context window: {{max_context_tokens}} tokens\n\n" | |
| "Current domain focus: {{domain_focus}}\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["max_context_tokens","domain_focus"], | |
| "tags": ["researcher","sub-agent","search","analysis","system"], | |
| "status": "approved", | |
| }, | |
| # CODER | |
| { | |
| "id": "coder_persona", | |
| "type": "system", | |
| "agent": "coder", | |
| "name": "CODER Sub-Agent Persona", | |
| "description": "System prompt for the coder sub-agent. Code generation, debugging, refactoring, and vault execution specialist.", | |
| "template": ( | |
| "You are CODER, a specialist sub-agent inside the FORGE AI ecosystem at ki-fusion-labs.de.\n\n" | |
| "Your strengths:\n" | |
| "- Python, JavaScript, bash scripting, SQL, Dockerfile authoring\n" | |
| "- Code review, bug detection, refactoring, and performance analysis\n" | |
| "- Executing code in agent-vault (sandboxed runtime)\n" | |
| "- Reading and writing files in the VAULT workspace\n\n" | |
| "Working style:\n" | |
| "- Always request VAULT execution approval for destructive operations (rm, git push main, DROP TABLE)\n" | |
| "- Write tests alongside implementation when the task warrants it\n" | |
| "- Output clean, commented, production-ready code\n" | |
| "- Log all vault_exec calls to agent-trace\n\n" | |
| "Tools available: vault_exec, vault_write, vault_read, git_commit, git_push\n\n" | |
| "Safety rules:\n" | |
| "- NEVER execute rm -rf, fork bombs, or network exfil without APPROVE gate\n" | |
| "- Scan all tool outputs via agent-harness before returning to PULSE\n\n" | |
| "Current task context: {{task_context}}\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["task_context"], | |
| "tags": ["coder","sub-agent","code","vault","execution","system"], | |
| "status": "approved", | |
| }, | |
| # PLANNER | |
| { | |
| "id": "planner_persona", | |
| "type": "system", | |
| "agent": "planner", | |
| "name": "PLANNER Sub-Agent Persona", | |
| "description": "System prompt for the planner sub-agent. Task decomposition, dependency mapping, sprint planning, and Kanban management.", | |
| "template": ( | |
| "You are PLANNER, a specialist sub-agent inside the FORGE AI ecosystem at ki-fusion-labs.de.\n\n" | |
| "Your strengths:\n" | |
| "- Breaking complex goals into ordered, dependency-tracked subtasks\n" | |
| "- Sprint planning and workload balancing across agents\n" | |
| "- Risk identification and mitigation planning\n" | |
| "- Kanban board management (agent-dispatch)\n\n" | |
| "Working style:\n" | |
| "- Every task gets: id, title, description, agent, est_minutes, deps, priority 1-5\n" | |
| "- Output task lists as JSON arrays — always machine-readable\n" | |
| "- Identify critical path and flag blockers immediately\n" | |
| "- Update KANBAN cards as tasks move through states\n\n" | |
| "Project context: {{project_name}}\n" | |
| "Active agents: {{active_agents}}\n" | |
| "Sprint goal: {{sprint_goal}}\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["project_name","active_agents","sprint_goal"], | |
| "tags": ["planner","sub-agent","planning","kanban","sprint","system"], | |
| "status": "approved", | |
| }, | |
| # MONITOR | |
| { | |
| "id": "monitor_persona", | |
| "type": "system", | |
| "agent": "monitor", | |
| "name": "MONITOR Sub-Agent Persona", | |
| "description": "System prompt for the monitor sub-agent. Health checks, anomaly detection, alert routing, and RLHF reward trend watching.", | |
| "template": ( | |
| "You are MONITOR, a specialist sub-agent inside the FORGE AI ecosystem at ki-fusion-labs.de.\n\n" | |
| "Your responsibilities:\n" | |
| "- Continuous health monitoring of all FORGE spaces\n" | |
| "- Anomaly detection: latency spikes, error rate surges, reward degradation\n" | |
| "- Alert routing to christof via RELAY when thresholds are breached\n" | |
| "- RLHF reward trend watching — flag agents with avg_reward < {{reward_threshold}}\n\n" | |
| "Alert thresholds:\n" | |
| "- Space unhealthy: HTTP non-2xx for 3+ consecutive checks\n" | |
| "- Latency spike: p95 > {{latency_p95_ms}}ms\n" | |
| "- Error rate: > {{error_rate_pct}}% in last 10 min\n" | |
| "- Reward drop: delta < -0.1 vs prior hour\n\n" | |
| "Alert format: send via relay_notify with priority=urgent and approval buttons if action needed.\n\n" | |
| "Check interval: {{check_interval_seconds}}s\n\n" | |
| "You are part of the FORGE ecosystem at ki-fusion-labs.de." | |
| ), | |
| "variables": ["reward_threshold","latency_p95_ms","error_rate_pct","check_interval_seconds"], | |
| "tags": ["monitor","sub-agent","health","alerts","observability","system"], | |
| "status": "approved", | |
| }, | |
| # CHRISTOF (operator persona) | |
| { | |
| "id": "christof_persona", | |
| "type": "persona", | |
| "agent": "christof", | |
| "name": "CHRISTOF Operator Profile", | |
| "description": "Operator profile for Christof Kleinmanns — used by agents to calibrate communication style, authority level, and escalation routing.", | |
| "template": ( | |
| "Operator: Christof Kleinmanns (Chris4K)\n" | |
| "Role: Digital Transformation Leader, bofrost* — International Project Lead\n" | |
| "Lab: ki-fusion-labs.de — RTX 5090 local inference, FORGE ecosystem owner\n\n" | |
| "Communication preferences:\n" | |
| "- Direct and technical — no fluff, no excessive preamble\n" | |
| "- German or English — match the language of the original message\n" | |
| "- Prefer structured output (JSON, tables, numbered lists) for decisions\n" | |
| "- Telegram for urgent alerts, internal RELAY for routine updates\n\n" | |
| "Authority level: FULL — can approve any action, override any agent\n" | |
| "Approval required for: git push main, rm -rf, external API calls with PII, " | |
| "any GDPR-relevant data operations\n\n" | |
| "Active projects:\n" | |
| "- FORGE AI ecosystem (HuggingFace Spaces)\n" | |
| "- bofrost* GDPR Löschkonzept (DE/AT/NL/BE/LUX/FR)\n" | |
| "- RTX 5090 BitNet / RLHF fine-tuning infrastructure\n\n" | |
| "Escalate to Christof when: safety gate triggered, budget exceeded, " | |
| "capability gap blocks critical task, or reward drops below 4.0 for 2+ hours." | |
| ), | |
| "variables": [], | |
| "tags": ["operator","christof","authority","escalation","persona"], | |
| "status": "approved", | |
| }, | |
| ] | |
| for s in seeds: | |
| ok, msg = prompt_create(s) | |
| if ok and s.get("status") == "approved": | |
| prompt_approve(s["id"]) | |
| # Create personas | |
| personas_data = [ | |
| {"agent":"nexus", "name":"NEXUS Router", "system_prompt_id":"nexus_router", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":1, | |
| "tools":["chat","slot_reserve","slot_release"], "config":{"trace":True}}, | |
| {"agent":"pulse", "name":"PULSE Scheduler", "system_prompt_id":"pulse_scheduler", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":8, | |
| "tools":["kanban_create","kanban_move","slot_reserve","trigger_agent","self_reflect"], | |
| "config":{"trace":True,"self_improve":True}}, | |
| {"agent":"loop", "name":"LOOP Orchestrator", "system_prompt_id":"loop_improvement_cycle", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":4, | |
| "tools":["fetch_stats","propose_prompt","notify"], "config":{"cycle_minutes":60}}, | |
| # Sprint 4 sub-agents | |
| {"agent":"researcher", "name":"RESEARCHER Sub-Agent", "system_prompt_id":"researcher_persona", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":6, | |
| "tools":["knowledge_search","knowledge_fetch","web_search","memory_store"], | |
| "config":{"trace":True,"default_domain_focus":"AI / ML / GDPR","max_context_tokens":50000}}, | |
| {"agent":"coder", "name":"CODER Sub-Agent", "system_prompt_id":"coder_persona", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":10, | |
| "tools":["vault_exec","vault_write","vault_read","git_commit","git_push","harness_scan"], | |
| "config":{"trace":True,"require_approval_for":["rm -rf","git push main","DROP TABLE"]}}, | |
| {"agent":"planner", "name":"PLANNER Sub-Agent", "system_prompt_id":"planner_persona", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":5, | |
| "tools":["kanban_create","kanban_move","kanban_list","relay_send"], | |
| "config":{"trace":True,"default_project":"FORGE","default_sprint_goal":"Ship Sprint 4"}}, | |
| {"agent":"monitor", "name":"MONITOR Sub-Agent", "system_prompt_id":"monitor_persona", | |
| "model_pref":"qwen/qwen3.5-35b-a3b", "max_steps":3, | |
| "tools":["health_check","relay_notify","learn_stats","trace_query"], | |
| "config":{"trace":True,"check_interval_seconds":60,"reward_threshold":4.0, | |
| "latency_p95_ms":8000,"error_rate_pct":15}}, | |
| {"agent":"christof", "name":"CHRISTOF Operator", "system_prompt_id":"christof_persona", | |
| "model_pref":"", "max_steps":0, | |
| "tools":[], | |
| "config":{"authority":"FULL","telegram_priority":True,"language":"auto"}}, | |
| ] | |
| for p in personas_data: | |
| try: persona_upsert(p) | |
| except Exception: pass | |
| # --------------------------------------------------------------------------- | |
| # MCP | |
| # --------------------------------------------------------------------------- | |
| MCP_TOOLS = [ | |
| {"name":"prompts_get","description":"Get a prompt by ID, with optional variable rendering.", | |
| "inputSchema":{"type":"object","required":["id"], | |
| "properties":{"id":{"type":"string"},"version":{"type":"integer"}, | |
| "variables":{"type":"object","description":"Template variables to substitute"}}}}, | |
| {"name":"prompts_search","description":"Search prompt library by agent, type, tag, or keyword.", | |
| "inputSchema":{"type":"object","properties":{"agent":{"type":"string"},"type":{"type":"string"}, | |
| "tag":{"type":"string"},"q":{"type":"string"}, | |
| "status":{"type":"string","default":"approved"},"limit":{"type":"integer"}}}}, | |
| {"name":"prompts_create","description":"Create a new prompt or new version of existing prompt.", | |
| "inputSchema":{"type":"object","required":["name","template"], | |
| "properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}, | |
| "agent":{"type":"string"},"template":{"type":"string"}, | |
| "variables":{"type":"array"},"tags":{"type":"array"}, | |
| "description":{"type":"string"},"status":{"type":"string"}}}}, | |
| {"name":"prompts_approve","description":"Approve a prompt version for production use.", | |
| "inputSchema":{"type":"object","required":["id"], | |
| "properties":{"id":{"type":"string"},"version":{"type":"integer"}}}}, | |
| {"name":"persona_get","description":"Get the active persona for an agent (includes rendered system prompt).", | |
| "inputSchema":{"type":"object","required":["agent"], | |
| "properties":{"agent":{"type":"string"}}}}, | |
| {"name":"persona_set","description":"Set or update an agent persona.", | |
| "inputSchema":{"type":"object","required":["agent","system_prompt_id"], | |
| "properties":{"agent":{"type":"string"},"system_prompt_id":{"type":"string"}, | |
| "name":{"type":"string"},"model_pref":{"type":"string"}, | |
| "max_steps":{"type":"integer"},"tools":{"type":"array"}}}}, | |
| {"name":"ab_record","description":"Record an A/B test outcome for a prompt version.", | |
| "inputSchema":{"type":"object","required":["prompt_id","version","agent","outcome"], | |
| "properties":{"prompt_id":{"type":"string"},"version":{"type":"integer"}, | |
| "agent":{"type":"string"},"session_id":{"type":"string"}, | |
| "outcome":{"type":"string","description":"success|failure|unknown"}, | |
| "latency_ms":{"type":"number"},"reward":{"type":"number"}}}}, | |
| {"name":"prompts_stats","description":"Get prompt registry statistics.", | |
| "inputSchema":{"type":"object","properties":{}}}, | |
| ] | |
| def handle_mcp(method, params, req_id): | |
| def ok(r): return {"jsonrpc":"2.0","id":req_id,"result":r} | |
| def txt(d): return ok({"content":[{"type":"text","text":json.dumps(d)}]}) | |
| if method=="initialize": | |
| return ok({"protocolVersion":"2024-11-05", | |
| "serverInfo":{"name":"agent-prompts","version":"1.0.0"}, | |
| "capabilities":{"tools":{}}}) | |
| if method=="tools/list": return ok({"tools":MCP_TOOLS}) | |
| if method=="tools/call": | |
| n, a = params.get("name",""), params.get("arguments",{}) | |
| if n=="prompts_get": | |
| r = prompt_render(a["id"], a.get("variables",{}), a.get("version",0)) | |
| return txt(r) | |
| if n=="prompts_search": | |
| rows = prompt_list(agent=a.get("agent",""), ptype=a.get("type",""), | |
| status=a.get("status","approved"), tag=a.get("tag",""), | |
| q=a.get("q",""), limit=a.get("limit",20)) | |
| return txt({"prompts":rows,"count":len(rows)}) | |
| if n=="prompts_create": | |
| ok2, msg = prompt_create(a) | |
| return txt({"ok":ok2,"message":msg}) | |
| if n=="prompts_approve": | |
| updated = prompt_approve(a["id"], a.get("version",0)) | |
| return txt({"ok":updated}) | |
| if n=="persona_get": | |
| p = persona_get(a["agent"]) | |
| return txt(p or {"error":f"No active persona for {a['agent']}"}) | |
| if n=="persona_set": | |
| pid = persona_upsert(a) | |
| return txt({"ok":True,"id":pid}) | |
| if n=="ab_record": | |
| ab_record(a["prompt_id"],a["version"],a["agent"], | |
| a.get("session_id",""),a.get("outcome","unknown"), | |
| a.get("latency_ms"),a.get("reward")) | |
| return txt({"ok":True}) | |
| if n=="prompts_stats": | |
| return txt(get_stats()) | |
| return {"jsonrpc":"2.0","id":req_id,"error":{"code":-32601,"message":f"Unknown tool: {n}"}} | |
| 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}"}} | |
| # --------------------------------------------------------------------------- | |
| # App | |
| # --------------------------------------------------------------------------- | |
| async def lifespan(app): | |
| init_db(); seed_prompts() | |
| yield | |
| app = FastAPI(title="agent-prompts", version="1.0.0", lifespan=lifespan) | |
| def _auth(r): return not PROMPTS_KEY or r.headers.get("x-prompts-key","") == PROMPTS_KEY | |
| # REST | |
| async def api_list(agent:str=Query(""), type:str=Query(""), status:str=Query("approved"), | |
| tag:str=Query(""), q:str=Query(""), limit:int=Query(50), offset:int=Query(0)): | |
| return JSONResponse({"prompts":prompt_list(agent,type,status,tag,q,limit,offset)}) | |
| async def api_render(pid:str, request:Request, version:int=Query(0)): | |
| vars_raw = dict(request.query_params) | |
| for skip in ("version",): vars_raw.pop(skip,None) | |
| r = prompt_render(pid, vars_raw, version) | |
| if "error" in r: raise HTTPException(404, r["error"]) | |
| return JSONResponse(r) | |
| async def api_versions(pid:str): return JSONResponse({"versions":prompt_versions(pid)}) | |
| async def api_ab(pid:str): return JSONResponse(ab_stats(pid)) | |
| async def api_get(pid:str, version:int=Query(0)): | |
| p = prompt_get(pid,version) | |
| if not p: raise HTTPException(404,f"Prompt '{pid}' not found") | |
| return JSONResponse(p) | |
| async def api_create(request:Request): | |
| if not _auth(request): raise HTTPException(403,"Invalid X-Prompts-Key") | |
| body = await request.json() | |
| ok2,msg = prompt_create(body) | |
| if not ok2: raise HTTPException(400,msg) | |
| return JSONResponse({"ok":True,"message":msg}) | |
| async def api_approve(pid:str, request:Request, version:int=Query(0)): | |
| if not _auth(request): raise HTTPException(403,"Invalid X-Prompts-Key") | |
| updated = prompt_approve(pid,version) | |
| return JSONResponse({"ok":updated}) | |
| async def api_deprecate(pid:str, request:Request): | |
| if not _auth(request): raise HTTPException(403,"Invalid X-Prompts-Key") | |
| updated = prompt_deprecate(pid) | |
| return JSONResponse({"ok":updated}) | |
| async def api_personas(): return JSONResponse({"personas":persona_list()}) | |
| async def api_persona(agent:str): | |
| p = persona_get(agent) | |
| if not p: raise HTTPException(404,f"No active persona for '{agent}'") | |
| return JSONResponse(p) | |
| async def api_persona_upsert(request:Request): | |
| """No auth required — agents and FORGE register personas freely.""" | |
| body = await request.json() | |
| try: | |
| pid = persona_upsert(body) | |
| except ValueError as e: | |
| raise HTTPException(400, str(e)) | |
| return JSONResponse({"ok":True,"id":pid}) | |
| async def api_ab_record(request:Request): | |
| body = await request.json() | |
| ab_record(body["prompt_id"],body["version"],body.get("agent",""), | |
| body.get("session_id",""),body.get("outcome","unknown"), | |
| body.get("latency_ms"),body.get("reward")) | |
| return JSONResponse({"ok":True}) | |
| async def api_stats(): return JSONResponse(get_stats()) | |
| async def api_health(): | |
| conn=get_db();n=conn.execute("SELECT COUNT(DISTINCT id) FROM prompts").fetchone()[0];conn.close() | |
| return JSONResponse({"ok":True,"prompts":n,"version":"1.0.0"}) | |
| async def mcp_sse(request:Request): | |
| async def gen(): | |
| yield f"data: {json.dumps({'jsonrpc':'2.0','method':'connected','params':{}})}\n\n" | |
| yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools','params':{'tools':MCP_TOOLS}})}\n\n" | |
| while True: | |
| if await request.is_disconnected(): break | |
| yield ": ping\n\n"; await asyncio.sleep(15) | |
| return StreamingResponse(gen(), media_type="text/event-stream", | |
| headers={"Cache-Control":"no-cache","Connection":"keep-alive","X-Accel-Buffering":"no"}) | |
| async def mcp_rpc(request:Request): | |
| try: body = await request.json() | |
| except Exception: return JSONResponse({"jsonrpc":"2.0","id":None,"error":{"code":-32700,"message":"Parse error"}}) | |
| if isinstance(body,list): | |
| return JSONResponse([r for r in [handle_mcp(x.get("method",""),x.get("params",{}),x.get("id")) for x in body] if r]) | |
| r = handle_mcp(body.get("method",""),body.get("params",{}),body.get("id")) | |
| return JSONResponse(r or {"jsonrpc":"2.0","id":body.get("id"),"result":{}}) | |
| # --------------------------------------------------------------------------- | |
| # SPA | |
| # --------------------------------------------------------------------------- | |
| SPA = r"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>💬 PROMPTS — FORGE Prompt 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:#06060d;--sf:#0d0d18;--sf2:#121222;--br:#1a1a2e;--ac:#ff6b00;--tx:#dde0f0;--mu:#50507a;--gr:#00ff88;--rd:#ff4455;--cy:#06b6d4;--pu:#8b5cf6;--ye:#f59e0b;--pk:#ec4899} | |
| html,body{height:100%;background:var(--bg);color:var(--tx);font-family:'Syne',sans-serif} | |
| ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--sf)}::-webkit-scrollbar-thumb{background:var(--br);border-radius:3px} | |
| .app{display:grid;grid-template-rows:52px auto 1fr;height:100vh;overflow:hidden} | |
| .hdr{display:flex;align-items:center;gap:1rem;padding:0 1.5rem;border-bottom:1px solid var(--br);background:var(--sf)} | |
| .logo{font-family:'Space Mono',monospace;font-size:1.1rem;font-weight:700;color:var(--ac)} | |
| .sub{font-family:'DM Mono',monospace;font-size:.6rem;color:var(--mu);letter-spacing:.2em;text-transform:uppercase} | |
| .hstats{display:flex;gap:1.5rem;margin-left:auto} | |
| .hs{text-align:center}.hs-n{font-family:'Space Mono',monospace;font-size:1rem;font-weight:700;color:var(--ac)} | |
| .hs-l{font-family:'DM Mono',monospace;font-size:.58rem;color:var(--mu);text-transform:uppercase;letter-spacing:.1em} | |
| .tabs{display:flex;border-bottom:1px solid var(--br);background:var(--sf);flex-shrink:0} | |
| .tab{padding:.55rem 1.3rem;font-family:'DM Mono',monospace;font-size:.72rem;color:var(--mu);border-bottom:2px solid transparent;cursor:pointer;letter-spacing:.05em;transition:all .15s} | |
| .tab.active{color:var(--ac);border-bottom-color:var(--ac)}.tab:hover{color:var(--tx)} | |
| .body{overflow-y:auto;padding:1.25rem} | |
| .main-layout{display:grid;grid-template-columns:260px 1fr;gap:1rem;height:100%} | |
| /* Sidebar */ | |
| .sidebar{display:flex;flex-direction:column;gap:.4rem} | |
| .search-input{width:100%;background:var(--sf);border:1px solid var(--br);color:var(--tx);padding:.5rem .75rem;border-radius:6px;font-family:'DM Mono',monospace;font-size:.78rem;outline:none;transition:border-color .15s} | |
| .search-input:focus{border-color:var(--ac)} | |
| .search-input::placeholder{color:var(--mu)} | |
| .filter-group{background:var(--sf);border:1px solid var(--br);border-radius:6px;overflow:hidden} | |
| .fg-label{font-family:'DM Mono',monospace;font-size:.6rem;color:var(--mu);text-transform:uppercase;letter-spacing:.15em;padding:.4rem .75rem;border-bottom:1px solid var(--br)} | |
| .filter-btn{display:flex;align-items:center;gap:.5rem;padding:.4rem .75rem;font-family:'DM Mono',monospace;font-size:.75rem;cursor:pointer;border:none;background:none;color:var(--tx);width:100%;text-align:left;transition:background .1s} | |
| .filter-btn:hover{background:var(--sf2)}.filter-btn.active{color:var(--ac);background:var(--sf2);border-left:2px solid var(--ac)} | |
| .type-pill{font-size:.58rem;padding:1px 6px;border-radius:10px;margin-left:auto} | |
| /* Card grid */ | |
| .cards{display:flex;flex-direction:column;gap:.6rem} | |
| .card{background:var(--sf);border:1px solid var(--br);border-radius:8px;padding:.9rem 1rem;cursor:pointer;transition:border-color .15s} | |
| .card:hover{border-color:var(--ac)}.card.active{border-color:var(--ac);border-width:2px} | |
| .card-row1{display:flex;align-items:center;gap:.6rem;margin-bottom:.25rem} | |
| .card-name{font-family:'Space Mono',monospace;font-size:.85rem;font-weight:700} | |
| .card-agent{font-family:'DM Mono',monospace;font-size:.62rem;color:var(--mu);margin-left:auto} | |
| .card-desc{font-size:.78rem;color:var(--mu);line-height:1.5} | |
| .card-tags{display:flex;gap:.3rem;flex-wrap:wrap;margin-top:.4rem} | |
| .tag{font-family:'DM Mono',monospace;font-size:.58rem;background:#1a1a30;border:1px solid #2a2a50;color:var(--pu);padding:1px 6px;border-radius:10px} | |
| /* Detail pane */ | |
| .detail{background:var(--sf);border:1px solid var(--br);border-radius:8px;padding:1.1rem;height:fit-content} | |
| .detail-hdr{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem} | |
| .detail-name{font-family:'Space Mono',monospace;font-size:1.1rem;font-weight:700} | |
| .detail-meta{font-family:'DM Mono',monospace;font-size:.65rem;color:var(--mu);margin-top:3px} | |
| .slabel{font-family:'DM Mono',monospace;font-size:.62rem;color:var(--pu);text-transform:uppercase;letter-spacing:.15em;margin:.75rem 0 .35rem} | |
| pre.tmpl{background:#0a0a14;border:1px solid var(--br);border-radius:6px;padding:.75rem;font-family:'DM Mono',monospace;font-size:.72rem;color:var(--gr);overflow-x:auto;white-space:pre-wrap;line-height:1.7;max-height:280px;overflow-y:auto} | |
| .var-chip{display:inline-block;background:#0a001a;border:1px solid #2a0066;color:var(--pu);font-family:'DM Mono',monospace;font-size:.65rem;padding:2px 8px;border-radius:4px;margin:2px} | |
| .render-row{display:flex;gap:.5rem;margin-top:.75rem} | |
| .render-input{flex:1;background:var(--sf2);border:1px solid var(--br);color:var(--tx);padding:.45rem .75rem;border-radius:5px;font-family:'DM Mono',monospace;font-size:.75rem;outline:none} | |
| .render-input:focus{border-color:var(--ac)} | |
| .render-input::placeholder{color:var(--mu)} | |
| .btn{padding:.4rem .9rem;border:none;border-radius:5px;cursor:pointer;font-family:'DM Mono',monospace;font-size:.7rem;font-weight:700;transition:all .15s} | |
| .btn-primary{background:var(--ac);color:#000}.btn-primary:hover{filter:brightness(1.1)} | |
| .btn-secondary{background:var(--sf2);color:var(--tx);border:1px solid var(--br)}.btn-secondary:hover{border-color:var(--ac);color:var(--ac)} | |
| .btn-approve{background:#001a08;color:var(--gr);border:1px solid #004422}.btn-approve:hover{background:#003010} | |
| .btn-deprecate{background:#1a0000;color:var(--rd);border:1px solid #440011} | |
| pre.rendered{background:#0a0a14;border:1px solid var(--gr);border-radius:6px;padding:.75rem;font-family:'DM Mono',monospace;font-size:.72rem;color:var(--gr);white-space:pre-wrap;line-height:1.7;margin-top:.5rem} | |
| /* Persona cards */ | |
| .persona-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.75rem} | |
| .persona-card{background:var(--sf);border:1px solid var(--br);border-radius:8px;padding:.9rem 1rem} | |
| .persona-name{font-family:'Space Mono',monospace;font-size:.9rem;font-weight:700;color:var(--ac);margin-bottom:.35rem} | |
| .persona-field{font-family:'DM Mono',monospace;font-size:.72rem;color:var(--mu);margin-bottom:.2rem} | |
| .persona-field span{color:var(--tx)} | |
| .persona-prompt{font-family:'DM Mono',monospace;font-size:.65rem;color:var(--mu);margin-top:.5rem;line-height:1.5;max-height:80px;overflow:hidden;text-overflow:ellipsis} | |
| /* New prompt form */ | |
| .form-group{margin-bottom:.85rem} | |
| .form-label{display:block;font-family:'DM Mono',monospace;font-size:.65rem;color:var(--mu);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.3rem} | |
| .form-control{width:100%;background:var(--sf);border:1px solid var(--br);color:var(--tx);padding:.5rem .75rem;border-radius:5px;font-family:'DM Mono',monospace;font-size:.78rem;outline:none;transition:border-color .15s} | |
| .form-control:focus{border-color:var(--ac)} | |
| .form-control option{background:var(--sf)} | |
| textarea.form-control{min-height:160px;resize:vertical;font-size:.73rem;line-height:1.7} | |
| .form-row{display:grid;grid-template-columns:1fr 1fr;gap:.75rem} | |
| .msg{padding:.5rem .75rem;border-radius:5px;font-family:'DM Mono',monospace;font-size:.75rem;margin-top:.5rem;display:none} | |
| .msg-ok{background:#001a08;border:1px solid var(--gr);color:var(--gr)} | |
| .msg-err{background:#1a0000;border:1px solid var(--rd);color:var(--rd)} | |
| /* Status badges */ | |
| .s-approved{color:var(--gr)}.s-draft{color:var(--ye)}.s-deprecated{color:var(--mu)} | |
| .empty{text-align:center;padding:2rem;color:var(--mu);font-family:'DM Mono',monospace;font-size:.8rem} | |
| </style></head><body> | |
| <div class="app"> | |
| <header class="hdr"> | |
| <div><div class="logo">💬 PROMPTS</div><div class="sub">FORGE Prompt & Persona Registry</div></div> | |
| <div class="hstats"> | |
| <div class="hs"><div class="hs-n" id="hP">—</div><div class="hs-l">Prompts</div></div> | |
| <div class="hs"><div class="hs-n" id="hA" style="color:var(--gr)">—</div><div class="hs-l">Approved</div></div> | |
| <div class="hs"><div class="hs-n" id="hPe" style="color:var(--pu)">—</div><div class="hs-l">Personas</div></div> | |
| <div class="hs"><div class="hs-n" id="hAB" style="color:var(--cy)">—</div><div class="hs-l">A/B results</div></div> | |
| </div> | |
| </header> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="showTab('browse')">🔎 Browse</div> | |
| <div class="tab" onclick="showTab('personas')">🤗 Personas</div> | |
| <div class="tab" onclick="showTab('new')">+ New Prompt</div> | |
| <div class="tab" onclick="showTab('api')">📡 API</div> | |
| </div> | |
| <div class="body" id="tabBody"></div> | |
| </div> | |
| <script> | |
| const TYPE_COLORS={system:'#ff6b00',user:'#8b5cf6',tool:'#06b6d4',chain:'#f59e0b',persona:'#ec4899',fragment:'#10b981'}; | |
| const STATUS_CLS={approved:'s-approved',draft:'s-draft',deprecated:'s-deprecated'}; | |
| let stats=null,prompts=[],selectedId=null,selectedType='',selectedAgent=''; | |
| async function loadStats(){ | |
| stats=await fetch('/api/stats').then(r=>r.json()); | |
| document.getElementById('hP').textContent=stats.total_prompts||0; | |
| document.getElementById('hA').textContent=stats.by_status?.approved||0; | |
| document.getElementById('hPe').textContent=stats.active_personas||0; | |
| document.getElementById('hAB').textContent=stats.ab_results||0; | |
| } | |
| async function loadPrompts(){ | |
| const p=new URLSearchParams({status:'',limit:100}); | |
| if(selectedType) p.set('type',selectedType); | |
| if(selectedAgent) p.set('agent',selectedAgent); | |
| prompts=(await fetch('/api/prompts?'+p).then(r=>r.json())).prompts||[]; | |
| renderCards(prompts); | |
| } | |
| function showTab(t){ | |
| document.querySelectorAll('.tab').forEach((el,i)=>el.classList.toggle('active',['browse','personas','new','api'][i]===t)); | |
| if(t==='browse') renderBrowse(); | |
| else if(t==='personas') renderPersonas(); | |
| else if(t==='new') renderNew(); | |
| else if(t==='api') renderAPI(); | |
| } | |
| // ---- BROWSE ---- | |
| function renderBrowse(){ | |
| document.getElementById('tabBody').innerHTML=` | |
| <div class="main-layout"> | |
| <div class="sidebar"> | |
| <input class="search-input" id="srch" placeholder="Search prompts..." oninput="filterSearch()"> | |
| <div class="filter-group"> | |
| <div class="fg-label">Type</div> | |
| ${['','system','user','tool','chain','persona','fragment'].map(t=>` | |
| <button class="filter-btn${selectedType===t?' active':''}" onclick="setType('${t}')"> | |
| ${t||'All types'} | |
| ${t?`<span class="type-pill" style="background:${TYPE_COLORS[t]}22;color:${TYPE_COLORS[t]}">${t}</span>`:''} | |
| </button>`).join('')} | |
| </div> | |
| <div class="filter-group"> | |
| <div class="fg-label">Agent</div> | |
| ${['','*','nexus','pulse','relay','loop'].map(a=>` | |
| <button class="filter-btn${selectedAgent===a?' active':''}" onclick="setAgent('${a}')"> | |
| ${a||'All agents'} | |
| </button>`).join('')} | |
| </div> | |
| </div> | |
| <div> | |
| <div class="cards" id="cardList"></div> | |
| </div> | |
| </div>`; | |
| loadPrompts(); | |
| } | |
| function setType(t){selectedType=t;loadPrompts()} | |
| function setAgent(a){selectedAgent=a;loadPrompts()} | |
| function filterSearch(){ | |
| const q=document.getElementById('srch').value.toLowerCase(); | |
| const filtered=q?prompts.filter(p=>p.name?.toLowerCase().includes(q)||p.description?.toLowerCase().includes(q)):prompts; | |
| renderCards(filtered); | |
| } | |
| function renderCards(list){ | |
| const cl=document.getElementById('cardList'); | |
| if(!list||!cl)return; | |
| if(!list.length){cl.innerHTML='<div class="empty">No prompts found.</div>';return} | |
| cl.innerHTML=list.map(p=>{ | |
| const col=TYPE_COLORS[p.type]||'#fff'; | |
| const tags=(p.tags||[]).slice(0,4).map(t=>`<span class="tag">${t}</span>`).join(''); | |
| return `<div class="card${p.id===selectedId?' active':''}" onclick="selectPrompt('${p.id}')"> | |
| <div class="card-row1"> | |
| <span style="color:${col};font-family:'DM Mono',monospace;font-size:.65rem;text-transform:uppercase;letter-spacing:.1em">${p.type}</span> | |
| <span class="${STATUS_CLS[p.status]||''}" style="font-family:'DM Mono',monospace;font-size:.6rem;margin-left:.5rem">● ${p.status}</span> | |
| <span class="card-agent">${p.agent}</span> | |
| </div> | |
| <div class="card-name">${esc(p.name)}</div> | |
| <div class="card-desc">${esc((p.description||'').slice(0,100))}${p.description?.length>100?'…':''}</div> | |
| <div class="card-tags">${tags}<span style="margin-left:auto;font-family:'DM Mono',monospace;font-size:.6rem;color:var(--mu)">${p.uses||0} uses · v${p.version}</span></div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| async function selectPrompt(id){ | |
| selectedId=id; | |
| renderCards(prompts); | |
| const p=await fetch(`/api/prompts/${id}`).then(r=>r.json()); | |
| const detail=document.querySelector('.main-layout > div:last-child'); | |
| if(!detail)return; | |
| const vars=(p.variables||[]); | |
| const col=TYPE_COLORS[p.type]||'#fff'; | |
| const varInputs=vars.map(v=>`<input class="render-input" id="var_${v}" placeholder="${v}">`).join(''); | |
| detail.innerHTML=`<div class="detail"> | |
| <div class="detail-hdr"> | |
| <div> | |
| <span style="color:${col};font-family:'DM Mono',monospace;font-size:.65rem;text-transform:uppercase;letter-spacing:.1em">${p.type}</span> | |
| <span class="${STATUS_CLS[p.status]||''}" style="font-family:'DM Mono',monospace;font-size:.6rem;margin-left:.75rem">● ${p.status}</span> | |
| </div> | |
| <div style="margin-left:auto;display:flex;gap:.4rem"> | |
| ${p.status!=='approved'?`<button class="btn btn-approve" onclick="approvePrompt('${id}')">✓ Approve</button>`:''} | |
| ${p.status!=='deprecated'?`<button class="btn btn-deprecate" onclick="deprecatePrompt('${id}')">✕ Deprecate</button>`:''} | |
| </div> | |
| </div> | |
| <div class="detail-name">${esc(p.name)}</div> | |
| <div class="detail-meta">ID: ${p.id} · v${p.version} · agent: ${p.agent} · ${p.uses||0} uses</div> | |
| ${p.description?`<p style="font-size:.8rem;color:var(--mu);margin:.5rem 0;line-height:1.6">${esc(p.description)}</p>`:''} | |
| <div class="slabel">Template</div> | |
| <pre class="tmpl">${esc(p.template||'')}</pre> | |
| ${vars.length?`<div class="slabel">Variables</div><div>${vars.map(v=>`<span class="var-chip">{{${v}}}</span>`).join('')}</div>`:''} | |
| ${vars.length?`<div class="slabel">Live Render</div> | |
| <div class="render-row">${varInputs}<button class="btn btn-primary" onclick="renderPrompt('${id}')">Render</button></div> | |
| <div id="renderOut"></div>`:''} | |
| ${p.tags?.length?`<div class="slabel">Tags</div><div>${(p.tags||[]).map(t=>`<span class="tag">${t}</span>`).join('')}</div>`:''} | |
| </div>`; | |
| } | |
| async function renderPrompt(id){ | |
| const p=prompts.find(x=>x.id===id)||{variables:[]}; | |
| const vars={}; | |
| (p.variables||[]).forEach(v=>{const el=document.getElementById('var_'+v);if(el)vars[v]=el.value}); | |
| const params=new URLSearchParams(vars); | |
| const r=await fetch(`/api/prompts/${id}/render?${params}`).then(x=>x.json()); | |
| const out=document.getElementById('renderOut'); | |
| if(out) out.innerHTML=r.rendered?`<pre class="rendered">${esc(r.rendered)}</pre>`:`<div class="msg msg-err" style="display:block">${r.error||'Error'}</div>`; | |
| } | |
| async function approvePrompt(id){ | |
| await fetch(`/api/prompts/${id}/approve`,{method:'POST'}); | |
| await loadStats();await loadPrompts();await selectPrompt(id); | |
| } | |
| async function deprecatePrompt(id){ | |
| await fetch(`/api/prompts/${id}/deprecate`,{method:'POST'}); | |
| await loadStats();await loadPrompts();renderCards(prompts); | |
| document.querySelector('.main-layout > div:last-child').innerHTML='<div class="empty">Select a prompt</div>'; | |
| } | |
| // ---- PERSONAS ---- | |
| async function renderPersonas(){ | |
| const personas=(await fetch('/api/personas').then(r=>r.json())).personas||[]; | |
| document.getElementById('tabBody').innerHTML=` | |
| <div class="persona-grid"> | |
| ${personas.length ? personas.map(p=>` | |
| <div class="persona-card"> | |
| <div class="persona-name">${esc(p.name)}</div> | |
| <div class="persona-field">Agent: <span style="color:var(--ac)">${p.agent}</span></div> | |
| <div class="persona-field">Prompt: <span onclick="showTab('browse');setTimeout(()=>selectPrompt('${p.system_prompt_id}'),300)" style="color:var(--cy);cursor:pointer">${p.system_prompt_id}</span></div> | |
| ${p.model_pref?`<div class="persona-field">Model: <span>${p.model_pref}</span></div>`:''} | |
| <div class="persona-field">Max steps: <span>${p.max_steps}</span></div> | |
| ${(p.tools||[]).length?`<div class="persona-field">Tools: <span style="font-size:.68rem">${p.tools.join(', ')}</span></div>`:''} | |
| <div class="persona-prompt">${esc((p.system_prompt||'').slice(0,150))}...</div> | |
| </div>`).join('') : '<div class="empty">No active personas</div>'} | |
| </div>`; | |
| } | |
| // ---- NEW PROMPT ---- | |
| function renderNew(){ | |
| document.getElementById('tabBody').innerHTML=` | |
| <div style="max-width:680px;margin:0 auto"> | |
| <div class="form-row"> | |
| <div class="form-group"><label class="form-label">ID (optional — auto-generated)</label><input class="form-control" id="nId" placeholder="my_prompt_id"></div> | |
| <div class="form-group"><label class="form-label">Version</label><input class="form-control" id="nVer" value="1" disabled style="opacity:.5"></div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"><label class="form-label">Type *</label> | |
| <select class="form-control" id="nType"> | |
| <option>system</option><option>user</option><option>tool</option><option>chain</option><option>persona</option><option>fragment</option> | |
| </select></div> | |
| <div class="form-group"><label class="form-label">Agent</label><input class="form-control" id="nAgent" value="*" placeholder="* = all agents"></div> | |
| </div> | |
| <div class="form-group"><label class="form-label">Name *</label><input class="form-control" id="nName" placeholder="Human-readable name"></div> | |
| <div class="form-group"><label class="form-label">Description</label><input class="form-control" id="nDesc" placeholder="What does this prompt do?"></div> | |
| <div class="form-group"><label class="form-label">Variables (comma-separated)</label><input class="form-control" id="nVars" placeholder="agent_name, max_steps, task"></div> | |
| <div class="form-group"><label class="form-label">Tags (comma-separated)</label><input class="form-control" id="nTags" placeholder="system, react, improvement"></div> | |
| <div class="form-group"><label class="form-label">Template * — use {{variable_name}} for substitution</label> | |
| <textarea class="form-control" id="nTmpl" style="min-height:220px" placeholder="You are {{agent_name}}..."></textarea></div> | |
| <div style="display:flex;gap:.5rem;align-items:center"> | |
| <button class="btn btn-primary" onclick="submitNew()">+ Create (Draft)</button> | |
| <button class="btn btn-approve" onclick="submitNew(true)">✓ Create & Approve</button> | |
| </div> | |
| <div class="msg" id="newMsg"></div> | |
| </div>`; | |
| } | |
| async function submitNew(approve=false){ | |
| const body={ | |
| id:document.getElementById('nId').value.trim()||undefined, | |
| type:document.getElementById('nType').value, | |
| agent:document.getElementById('nAgent').value.trim()||'*', | |
| name:document.getElementById('nName').value.trim(), | |
| description:document.getElementById('nDesc').value.trim(), | |
| variables:document.getElementById('nVars').value.split(',').map(v=>v.trim()).filter(Boolean), | |
| tags:document.getElementById('nTags').value.split(',').map(t=>t.trim()).filter(Boolean), | |
| template:document.getElementById('nTmpl').value, | |
| status:approve?'approved':'draft', | |
| }; | |
| const r=await fetch('/api/prompts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); | |
| const d=await r.json(); | |
| const msg=document.getElementById('newMsg'); | |
| msg.className='msg '+(r.ok?'msg-ok':'msg-err'); | |
| msg.textContent=(r.ok?'✓ ':'✗ ')+(d.message||'Error'); | |
| msg.style.display='block'; | |
| if(r.ok){setTimeout(()=>{msg.style.display='none'},4000);await loadStats();} | |
| } | |
| // ---- API ---- | |
| function renderAPI(){ | |
| document.getElementById('tabBody').innerHTML=` | |
| <div style="max-width:720px"> | |
| <p style="font-family:'DM Mono',monospace;font-size:.78rem;color:var(--mu);margin-bottom:1rem">Base: <span style="color:var(--ac2)">${window.location.origin}</span></p> | |
| ${[ | |
| ['GET', '/api/prompts', 'List. Params: agent, type, status, tag, q, limit'], | |
| ['GET', '/api/prompts/{id}', 'Get prompt (latest version)'], | |
| ['GET', '/api/prompts/{id}/render', 'Render with variables as query params'], | |
| ['GET', '/api/prompts/{id}/versions', 'Version history'], | |
| ['GET', '/api/prompts/{id}/ab', 'A/B test stats'], | |
| ['POST','/api/prompts', 'Create prompt. Body: {name, template, type, agent, variables, tags, status}'], | |
| ['POST','/api/prompts/{id}/approve', 'Approve for production'], | |
| ['POST','/api/prompts/{id}/deprecate', 'Deprecate'], | |
| ['GET', '/api/personas', 'List active personas'], | |
| ['GET', '/api/personas/{agent}', 'Get persona (with rendered system prompt)'], | |
| ['POST','/api/personas', 'Upsert persona'], | |
| ['POST','/api/ab', 'Record A/B result'], | |
| ['GET', '/api/stats', 'Registry stats'], | |
| ].map(([m,p,d])=>`<div style="display:grid;grid-template-columns:50px 280px 1fr;gap:.5rem .75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--br);font-family:'DM Mono',monospace;font-size:.73rem"> | |
| <span style="color:${m==='GET'?'var(--gr)':'var(--ye)'}">${m}</span> | |
| <code style="color:var(--cy)">${p}</code> | |
| <span style="color:var(--mu)">${d}</span> | |
| </div>`).join('')} | |
| <div style="margin-top:1.25rem;font-family:'DM Mono',monospace;font-size:.7rem;color:var(--pu);text-transform:uppercase;letter-spacing:.15em;margin-bottom:.4rem">Integration pattern (any existing space)</div> | |
| <pre style="background:var(--sf);border:1px solid var(--br);border-radius:6px;padding:.75rem;font-family:'DM Mono',monospace;font-size:.7rem;color:var(--gr);white-space:pre-wrap">PROMPTS_URL = os.getenv("PROMPTS_URL", "https://chris4k-agent-prompts.hf.space") | |
| # At startup — fetch and cache persona (never blocks if down) | |
| _cached_persona = None | |
| def get_system_prompt(fallback: str) -> str: | |
| global _cached_persona | |
| if _cached_persona: return _cached_persona | |
| try: | |
| r = requests.get(f"{PROMPTS_URL}/api/personas/nexus", timeout=3) | |
| _cached_persona = r.json().get("system_prompt", fallback) | |
| return _cached_persona | |
| except Exception: | |
| return fallback # ← always safe | |
| # At runtime — render a prompt with variables | |
| def render_prompt(prompt_id: str, **vars) -> str: | |
| try: | |
| r = requests.get(f"{PROMPTS_URL}/api/prompts/{prompt_id}/render", | |
| params=vars, timeout=3) | |
| return r.json().get("rendered", "") | |
| except Exception: | |
| return ""</pre> | |
| <div style="margin-top:1rem;font-family:'DM Mono',monospace;font-size:.7rem;color:var(--pu);text-transform:uppercase;letter-spacing:.15em;margin-bottom:.4rem">MCP config</div> | |
| <pre style="background:var(--sf);border:1px solid var(--br);border-radius:6px;padding:.75rem;font-family:'DM Mono',monospace;font-size:.7rem;color:var(--cy)">{"mcpServers":{"prompts":{"command":"npx","args":["-y","mcp-remote","${window.location.origin}/mcp/sse"]}}}</pre> | |
| </div>`; | |
| } | |
| function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')} | |
| loadStats();renderBrowse(); | |
| </script> | |
| </body></html>""" | |
| async def root(): return HTMLResponse(content=SPA, media_type="text/html; charset=utf-8") | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") |