""" PromptHub ⚡ Forge any AI agent from .md, .skill, or any text file. Drop files → fill system prompt → chat → share to the Collection. Storage: HF Bucket — auto-mounted at /data inside the Space. External access: hf sync hf://buckets/aidn/AnyAgent-storage ./local """ import os import re import json import datetime import gradio as gr from huggingface_hub import InferenceClient # ── Config ──────────────────────────────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8") BUCKET_DIR = "/data" AGENTS_FILE = os.path.join(BUCKET_DIR, "agents.jsonl") os.makedirs(BUCKET_DIR, exist_ok=True) # ── Storage ─────────────────────────────────────────────────────────────────── def _fetch_all() -> list: if not os.path.exists(AGENTS_FILE): return [] entries = [] try: with open(AGENTS_FILE, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: try: entries.append(json.loads(line)) except Exception: pass except Exception: pass return entries def _push_agent(agent: dict) -> str: try: with open(AGENTS_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(agent, ensure_ascii=False) + "\n") return "" except Exception as e: return str(e) def _collection_agents() -> list: return [a for a in _fetch_all() if a.get("in_collection", False)] # ── File Processing ─────────────────────────────────────────────────────────── def files_to_prompt(files, current: str) -> str: if not files: return current parts = [current.strip()] if current.strip() else [] for f in files: try: path = f.name if hasattr(f, "name") else str(f) with open(path, "r", encoding="utf-8", errors="ignore") as fh: content = fh.read().strip() fname = os.path.basename(path) parts.append(f"\n{content}") except Exception as e: parts.append(f"") return "\n\n---\n\n".join(parts) # ── Chat ────────────────────────────────────────────────────────────────────── def _bot_stream(history: list, system_prompt: str, max_tokens: int): if not history: return if not HF_TOKEN: history[-1]["content"] = "⚠️ No HF_TOKEN found. Add it in Settings → Secrets." yield history return sys_prompt = system_prompt.strip() or "You are a helpful assistant." msgs = [{"role": "system", "content": sys_prompt}] for m in history[:-1]: msgs.append({"role": m["role"], "content": m["content"] or ""}) msgs.append({"role": "user", "content": history[-2]["content"]}) client = InferenceClient(provider="novita", api_key=HF_TOKEN) try: for chunk in client.chat.completions.create( model=MODEL_ID, messages=msgs, max_tokens=max_tokens, stream=True, ): delta = chunk.choices[0].delta.content or "" history[-1]["content"] += delta yield history except Exception as e: history[-1]["content"] += f"\n\n⚠️ Error: {e}" yield history # ── Activate Agent ──────────────────────────────────────────────────────────── def activate_agent(name: str, prompt: str, in_coll: bool): name = name.strip() or "Unnamed Agent" prompt = prompt.strip() if not prompt: return ( "", _status_html("⚠️ Please add a system prompt before activating.", "warn"), _render_collection(), _render_quick_list(), "No agent active", ) status_msg = f'✅ "{name}" activated!' if in_coll: agent = { "name": name, "system_prompt": prompt, "in_collection": True, "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), } err = _push_agent(agent) if err: status_msg += f'
⚠️ Bucket write failed: {err}' else: status_msg += ( '  🌐 Saved to collection ' '· /data/agents.jsonl' ) return ( prompt, _status_html(status_msg, "ok"), _render_collection(), _render_quick_list(), name, ) # ── Load from collection (JS → Python via hidden state) ────────────────────── def _load_signal(signal: str): """Parse JSON signal from agentForgeLoad() JS call.""" if not signal or not signal.strip(): return gr.update(), gr.update(), gr.update(), gr.update() try: data = json.loads(signal) name = data.get("name", "") prompt = data.get("prompt", "") return name, prompt, prompt, name except Exception: return gr.update(), gr.update(), gr.update(), gr.update() # ── Status HTML ─────────────────────────────────────────────────────────────── def _status_html(msg: str, kind: str = "ok") -> str: colors = { "ok": ("rgba(74,222,128,.10)", "#4ade80", "#a7f3d0"), "warn": ("rgba(251,191,36,.10)", "#fbbf24", "#fde68a"), "err": ("rgba(248,113,113,.10)", "#f87171", "#fca5a5"), } bg, border, text = colors.get(kind, colors["ok"]) return ( f'
' f'{msg}
' ) # ── Rendering ───────────────────────────────────────────────────────────────── _PALETTE = ["#f97316", "#a78bfa", "#38bdf8", "#4ade80", "#fb7185", "#fbbf24"] def _agent_color(i: int) -> str: return _PALETTE[i % len(_PALETTE)] def _prompt_preview(prompt: str, max_len: int = 90) -> str: """One-line preview: strip all newlines, truncate.""" clean = re.sub(r'\s+', ' ', prompt).strip() return (clean[:max_len] + "…") if len(clean) > max_len else clean def _js_escape(s: str) -> str: """Escape a string for safe embedding inside JS single-quoted string.""" return (s .replace("\\", "\\\\") .replace("'", "\\'") .replace("\n", "\\n") .replace("\r", "") .replace('"', '\\"')) def _render_collection() -> str: agents = _collection_agents() count = len(agents) total_count = len(_fetch_all()) bucket_badge = ( f'' f'🗄️ /data · {total_count}' ) refresh_btn = ( '' ) if not agents: return ( '
' '
⚗️
' '
The forge is cold.
' '
' 'Activate an agent and check Add to Collection' ' to be the first.
' ) cards = "" for i, agent in enumerate(reversed(agents)): name = agent.get("name", "Unnamed Agent") prompt = agent.get("system_prompt", "") date = agent.get("timestamp", "")[:10] color = _agent_color(i) chars = len(prompt) preview = _prompt_preview(prompt, 80) n_js = _js_escape(name) p_js = _js_escape(prompt) safe_preview = preview.replace("<", "<").replace(">", ">") cards += ( f'
' f'
' f'{name}' f'{date}' f'
' # Expandable preview using
f'
' f'{safe_preview}' f'
' f'{prompt[:800].replace("<","<").replace(">",">")}{"…" if len(prompt)>800 else ""}' f'
' f'
' f'' f'Load into Forge →' f'{chars:,} chars' f'
' f'
' ) return ( f'
' f'
' f'⚡ {count} Agent{"s" if count!=1 else ""} Forged' f'
' f'{bucket_badge}{refresh_btn}' f'
' f'{cards}' f'
' ) def _render_quick_list() -> str: agents = _collection_agents() if not agents: return '
No agents yet — forge the first one! ⚡
' items = "" for i, agent in enumerate(reversed(agents[:10])): name = agent.get("name", "Unnamed") prompt = agent.get("system_prompt", "") color = _agent_color(i) n_js = _js_escape(name) p_js = _js_escape(prompt) items += ( f'
' f'
' f'{name}' f'Load →' f'
' ) if len(agents) > 10: items += f'
+ {len(agents)-10} more
' return items def _render_active_badge(name: str) -> str: if not name or name == "No agent active": return '
No agent active
' safe = name[:40] + ("…" if len(name) > 40 else "") return f'
Active: {safe}
' # ── JS ──────────────────────────────────────────────────────────────────────── # NOTE: head= is passed to demo.launch() in Gradio 6 JS_HEAD = r""" """ # ── CSS ─────────────────────────────────────────────────────────────────────── # Korrigierte Chatbot‑Darstellung (kein vertikales Buchstabieren mehr) CSS = """ :root { --bg: #08080f; --card: #0d0d1a; --card2: #11111e; --border: #1a1a2e; --border2: #222235; --orange: #f97316; --orange-dim: rgba(249,115,22,.15); --orange-glow:rgba(249,115,22,.25); --text: #e0e0f0; --muted: #44445a; --muted2: #888; --green: #4ade80; --font-head: 'Chakra Petch', sans-serif; --font-mono: 'JetBrains Mono', monospace; } body, .gradio-container { background: var(--bg) !important; font-family: var(--font-head) !important; color: var(--text) !important; } .block { background: var(--card) !important; border-color: var(--border) !important; } /* ── Header ── */ .forge-header { background: linear-gradient(135deg, #0d0d1a 0%, #130a20 50%, #0a1020 100%); border: 1px solid var(--border2); border-radius: 14px; padding: 24px 28px; margin-bottom: 16px; position: relative; overflow: hidden; } .forge-header::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse at 15% 60%, rgba(249,115,22,.10) 0%, transparent 55%), radial-gradient(ellipse at 85% 40%, rgba(167,139,250,.07) 0%, transparent 55%); pointer-events: none; } .forge-header h1 { font-family: var(--font-head) !important; font-size: 1.9rem !important; font-weight: 800 !important; color: var(--text) !important; margin: 0 0 4px 0 !important; position: relative; } .forge-header p { font-size: .8rem !important; color: var(--muted2) !important; margin: 0 !important; position: relative; font-family: var(--font-mono) !important; } /* ── Agent badge ── */ .agent-badge { display: inline-flex; align-items: center; gap: 7px; border-radius: 99px; padding: 5px 14px; font-size: .78rem; font-weight: 700; font-family: var(--font-head); border: 1px solid; margin-bottom: 8px; } .agent-badge.active { background: rgba(249,115,22,.08); border-color: rgba(249,115,22,.3); color: var(--orange); } .agent-badge.inactive { background: rgba(85,85,112,.06); border-color: var(--border2); color: var(--muted2); } .dot-active { width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 7px var(--green);animation:pulse-dot 2s ease-in-out infinite; } .dot-inactive { width:7px;height:7px;border-radius:50%;background:var(--muted); } @keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:.3} } /* ── Labels ── */ label > span { font-weight: 700 !important; font-size: .67rem !important; text-transform: uppercase !important; letter-spacing: .6px !important; color: var(--orange) !important; font-family: var(--font-head) !important; } /* ── Inputs ── */ textarea, input[type="text"] { background: var(--card) !important; color: var(--text) !important; border: 1px solid var(--border2) !important; border-radius: 8px !important; font-size: .87rem !important; font-family: var(--font-mono) !important; writing-mode: horizontal-tb !important; } textarea:focus, input[type="text"]:focus { border-color: var(--orange) !important; box-shadow: 0 0 0 2px var(--orange-dim) !important; outline: none !important; } textarea::placeholder, input[type="text"]::placeholder { color: var(--muted) !important; font-family: var(--font-mono) !important; } /* ── Chatbot – FIX: horizontale, lesbare Nachrichten ── */ .chatbot .message, .chatbot .bubble, .chatbot [data-testid="bot"], .chatbot [data-testid="user"] { writing-mode: horizontal-tb !important; white-space: pre-wrap !important; word-break: break-word !important; overflow-wrap: break-word !important; max-width: 85% !important; width: fit-content !important; } .chatbot .message p, .chatbot .bubble p, .chatbot [data-testid="bot"] p, .chatbot [data-testid="user"] p { white-space: pre-wrap !important; word-break: break-word !important; display: block !important; } .chatbot * { writing-mode: horizontal-tb !important; } /* ── File drop ── */ .file-upload-container, .upload-box { background: var(--card) !important; border: 2px dashed var(--border2) !important; border-radius: 10px !important; transition: all .2s !important; } .file-upload-container:hover, .upload-box:hover { border-color: var(--orange) !important; background: var(--orange-dim) !important; } /* ── Checkbox ── */ input[type="checkbox"] { accent-color: var(--orange) !important; } /* ── Buttons ── */ button.primary { background: linear-gradient(135deg, var(--orange), #c2410c) !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; font-family: var(--font-head) !important; color: #fff !important; box-shadow: 0 4px 16px var(--orange-glow) !important; transition: all .15s !important; } button.primary:hover { box-shadow: 0 6px 22px rgba(249,115,22,.45) !important; } button.secondary { background: transparent !important; border: 1px solid var(--border2) !important; color: var(--muted2) !important; border-radius: 8px !important; font-family: var(--font-head) !important; font-weight: 600 !important; transition: all .15s !important; } button.secondary:hover { border-color: var(--orange) !important; color: var(--orange) !important; } /* ── Accordion ── */ .accordion > .label-wrap { background: var(--card2) !important; border-color: var(--border) !important; color: var(--text) !important; font-family: var(--font-head) !important; font-size: .8rem !important; font-weight: 700 !important; } .accordion > .label-wrap:hover { background: var(--card) !important; } /* ── Tabs ── */ .tab-nav button { font-family: var(--font-head) !important; font-weight: 700 !important; font-size: .75rem !important; text-transform: uppercase !important; letter-spacing: .4px !important; color: var(--muted2) !important; background: transparent !important; border-bottom: 2px solid transparent !important; transition: all .15s !important; } .tab-nav button.selected { color: var(--orange) !important; border-bottom-color: var(--orange) !important; } /* ── Hidden helpers ── */ #hidden_refresh_btn { display: none !important; } #agent_load_signal { position: absolute !important; opacity: 0 !important; pointer-events: none !important; height: 0 !important; overflow: hidden !important; width: 1px !important; } footer { display: none !important; } """ # ── Build UI ────────────────────────────────────────────────────────────────── with gr.Blocks(title="PromptHub ⚡") as demo: system_prompt_state = gr.State("") active_name_state = gr.State("No agent active") gr.HTML("""

PromptHub

Drop .md · .skill · .txt · any text file → fill system prompt → chat → share to the Collection

""") with gr.Row(equal_height=False): # ── LEFT ────────────────────────────────────────────────────────────── with gr.Column(scale=4, min_width=280): gr.HTML('
📁 Drop Files
') file_upload = gr.File( label="", file_count="multiple", file_types=[".md", ".txt", ".skill", ".yaml", ".yml", ".json", ".py", ".ts", ".js", ".toml"], height=110, ) prompt_box = gr.Textbox( label="System Prompt", placeholder="Paste or type your system prompt…\nOr drop files above ↑\n\nTip: multiple files are auto-concatenated.", lines=9, max_lines=22, ) agent_name_box = gr.Textbox( label="Agent Name", placeholder="e.g. Code Reviewer · Sales Coach · SQL Expert", max_lines=1, ) in_coll_check = gr.Checkbox( label="🌐 Add to public Agent Collection", value=False, info="Saves the raw system prompt string to the shared HF Bucket.", ) activate_btn = gr.Button("⚡ Activate Agent", variant="primary", size="lg") status_out = gr.HTML("") with gr.Accordion("⚙️ Model Settings", open=False): max_tokens_slider = gr.Slider( minimum=256, maximum=8192, value=1024, step=128, label="Max output tokens", ) gr.Markdown(f"*Model: `{MODEL_ID}`*") gr.HTML("
") with gr.Accordion("📚 Quick Load from Collection", open=True): quick_list_html = gr.HTML(_render_quick_list()) # Signal‑Textbox (unsichtbar, aber aktiv) agent_load_signal = gr.Textbox( value="", elem_id="agent_load_signal", label="", visible=True, ) # ── RIGHT ───────────────────────────────────────────────────────────── with gr.Column(scale=6): active_badge_html = gr.HTML(_render_active_badge("No agent active")) chatbot = gr.Chatbot( height=440, show_label=False, ) # Chat input row with gr.Row(): msg_box = gr.Textbox( placeholder="Message your agent…", show_label=False, scale=7, max_lines=4, ) send_btn = gr.Button("Send", variant="primary", scale=2, min_width=80) clear_btn = gr.Button("Clear", variant="secondary", scale=1, min_width=70) # ── Tabs ────────────────────────────────────────────────────────────────── gr.HTML("
") with gr.Tabs(): with gr.Tab("🌐 Agent Collection"): collection_out = gr.HTML(_render_collection()) hidden_refresh_btn = gr.Button("refresh", elem_id="hidden_refresh_btn", visible=False) with gr.Tab("ℹ️ About"): gr.Markdown(""" ## PromptHub ⚡ **Forge any AI agent from plain text files.** Drop `.md`, `.skill`, `.txt`, `.yaml`, or any text file — PromptHub reads them, concatenates their content, and uses it as the system prompt for a live chat session. ### How it works 1. **Drop files** or paste text directly into the *System Prompt* box 2. Give your agent a **name** 3. Optionally **add it to the Collection** so others can load and use it 4. Hit **⚡ Activate Agent** — then chat! ### Storage All agents are stored in `/data/agents.jsonl` via HF Bucket (auto-mounted). ```bash # Download the bucket hf sync hf://buckets/aidn/AnyAgent-storage ./local ``` ### Secrets | Secret | Purpose | |--------|---------| | `HF_TOKEN` | Required for inference | | `MODEL_ID` | Optional model override | """) if not HF_TOKEN: gr.HTML(_status_html( "No HF_TOKEN — add it in Settings → Secrets.", "warn")) # ── Events ──────────────────────────────────────────────────────────────── file_upload.change( fn=files_to_prompt, inputs=[file_upload, prompt_box], outputs=[prompt_box], ) activate_btn.click( fn=activate_agent, inputs=[agent_name_box, prompt_box, in_coll_check], outputs=[system_prompt_state, status_out, collection_out, quick_list_html, active_name_state], ).then( fn=_render_active_badge, inputs=[active_name_state], outputs=[active_badge_html], ) def _submit(msg, hist): if not msg.strip(): return gr.update(), hist return "", hist + [ {"role": "user", "content": msg}, {"role": "assistant", "content": ""}, ] msg_box.submit( fn=_submit, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot], ).then( fn=_bot_stream, inputs=[chatbot, system_prompt_state, max_tokens_slider], outputs=[chatbot], ) send_btn.click( fn=_submit, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot], ).then( fn=_bot_stream, inputs=[chatbot, system_prompt_state, max_tokens_slider], outputs=[chatbot], ) clear_btn.click(fn=lambda: [], outputs=[chatbot]) agent_load_signal.change( fn=_load_signal, inputs=[agent_load_signal], outputs=[agent_name_box, prompt_box, system_prompt_state, active_name_state], ).then( fn=_render_active_badge, inputs=[active_name_state], outputs=[active_badge_html], ) hidden_refresh_btn.click( fn=lambda: (_render_collection(), _render_quick_list()), outputs=[collection_out, quick_list_html], ) if __name__ == "__main__": demo.launch( css=CSS, head=JS_HEAD, )