agent-prompts / main.py
Chris4K's picture
Update main.py
15dbe96 verified
"""
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": (
"&#9728;&#65039; *FORGE {{agent}}*\n"
"&#128197; {{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
# ---------------------------------------------------------------------------
@asynccontextmanager
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
@app.get("/api/prompts")
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)})
@app.get("/api/prompts/{pid}/render")
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)
@app.get("/api/prompts/{pid}/versions")
async def api_versions(pid:str): return JSONResponse({"versions":prompt_versions(pid)})
@app.get("/api/prompts/{pid}/ab")
async def api_ab(pid:str): return JSONResponse(ab_stats(pid))
@app.get("/api/prompts/{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)
@app.post("/api/prompts", status_code=201)
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})
@app.post("/api/prompts/{pid}/approve")
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})
@app.post("/api/prompts/{pid}/deprecate")
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})
@app.get("/api/personas")
async def api_personas(): return JSONResponse({"personas":persona_list()})
@app.get("/api/personas/{agent}")
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)
@app.post("/api/personas", status_code=201)
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})
@app.post("/api/ab", status_code=201)
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})
@app.get("/api/stats")
async def api_stats(): return JSONResponse(get_stats())
@app.get("/api/health")
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"})
@app.get("/mcp/sse")
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"})
@app.post("/mcp")
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>&#128172; PROMPTS &#8212; 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">&#128172; PROMPTS</div><div class="sub">FORGE Prompt &amp; Persona Registry</div></div>
<div class="hstats">
<div class="hs"><div class="hs-n" id="hP">&#8212;</div><div class="hs-l">Prompts</div></div>
<div class="hs"><div class="hs-n" id="hA" style="color:var(--gr)">&#8212;</div><div class="hs-l">Approved</div></div>
<div class="hs"><div class="hs-n" id="hPe" style="color:var(--pu)">&#8212;</div><div class="hs-l">Personas</div></div>
<div class="hs"><div class="hs-n" id="hAB" style="color:var(--cy)">&#8212;</div><div class="hs-l">A/B results</div></div>
</div>
</header>
<div class="tabs">
<div class="tab active" onclick="showTab('browse')">&#128270; Browse</div>
<div class="tab" onclick="showTab('personas')">&#129303; Personas</div>
<div class="tab" onclick="showTab('new')">&#43; New Prompt</div>
<div class="tab" onclick="showTab('api')">&#128225; 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">&#9679; ${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?'&hellip;':''}</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 &middot; 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">&#9679; ${p.status}</span>
</div>
<div style="margin-left:auto;display:flex;gap:.4rem">
${p.status!=='approved'?`<button class="btn btn-approve" onclick="approvePrompt('${id}')">&#10003; Approve</button>`:''}
${p.status!=='deprecated'?`<button class="btn btn-deprecate" onclick="deprecatePrompt('${id}')">&#10005; Deprecate</button>`:''}
</div>
</div>
<div class="detail-name">${esc(p.name)}</div>
<div class="detail-meta">ID: ${p.id} &middot; v${p.version} &middot; agent: ${p.agent} &middot; ${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()">&#43; Create (Draft)</button>
<button class="btn btn-approve" onclick="submitNew(true)">&#10003; Create &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
loadStats();renderBrowse();
</script>
</body></html>"""
@app.get("/", response_class=HTMLResponse)
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")