import json import os import threading from pathlib import Path import requests import gradio as gr import uvicorn from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse from gradio.routes import mount_gradio_app STOPWORDS = frozenset( "a an the and or but if then else when while of at by for with about into " "through during before after above below to from up down in out on off over " "under again further once here there all any both each few more most other " "some such no nor not only own same so than too very just can will should " "could would may might must shall is are was were be been being have has had " "having do does did doing it its this that these those i you he she we they " "me him her us them my your his their our mine yours ours theirs as".split() ) MAX_SKILL_CHARS = 6000 SKILLS_ROOT = Path("/app/skills/ethskills") STATE_FILE = Path("/app/skills_state.json") LLAMA_SERVER_URL = "http://127.0.0.1:8080/v1/chat/completions" LLAMA_MODELS_URL = "http://127.0.0.1:8080/v1/models" PUBLIC_MODEL_ID = "qwen2.5-7b-instruct-q3_k_m" CSS = """ :root { --app-max-width: 1220px; --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; --bg: #0b1220; --bg-soft: #0f1728; --panel: rgba(15, 23, 40, 0.84); --panel-2: rgba(18, 28, 48, 0.92); --panel-3: #10192b; --border: rgba(255, 255, 255, 0.09); --border-strong: rgba(255, 255, 255, 0.14); --text: #edf2ff; --text-muted: #9ca9bf; --text-faint: #7f8aa3; --accent: #12b981; --accent-hover: #0f9f70; --accent-soft: rgba(18, 185, 129, 0.14); --accent-strong: rgba(18, 185, 129, 0.26); --warning: #f59e0b; --warning-soft: rgba(245, 158, 11, 0.14); --danger: #ef4444; --danger-soft: rgba(239, 68, 68, 0.14); --radius-sm: 12px; --radius-md: 16px; --radius-lg: 22px; --shadow-sm: 0 8px 24px rgba(0, 0, 0, 0.18); --shadow-md: 0 18px 48px rgba(0, 0, 0, 0.28); } html, body { margin: 0 !important; padding: 0 !important; background: #0b1220 !important; } body, .gradio-container, .gradio-container *, .gradio-container .prose, .gradio-container .gr-markdown, .gradio-container .gr-markdown p, .gradio-container .gr-markdown h1, .gradio-container .gr-markdown h2, .gradio-container .gr-markdown h3, .gradio-container .gr-markdown ul, .gradio-container .gr-markdown li { color: var(--text) !important; } .gradio-container { max-width: 100% !important; min-height: 100vh; padding: 20px 14px 40px !important; background: radial-gradient(circle at top left, rgba(18, 185, 129, 0.12), transparent 28%), radial-gradient(circle at top right, rgba(56, 189, 248, 0.10), transparent 22%), linear-gradient(180deg, #0a1020 0%, #0b1220 100%) !important; color: var(--text) !important; } .gradio-container > .main, .gradio-container .main, .gradio-container .contain, .gradio-container .wrap, .gradio-container .block, .gradio-container .gr-block, .gradio-container .gr-panel, .gradio-container .gr-box, .gradio-container .gr-form { background: transparent !important; border: 0 !important; box-shadow: none !important; } .gradio-container .wrap, .gradio-container .contain { max-width: 100% !important; margin: 0 !important; padding: 0 !important; } .gradio-container .prose { max-width: none !important; } .gradio-container .gr-group, .gradio-container .gr-accordion { background: transparent !important; border: 0 !important; box-shadow: none !important; } .gradio-container footer { display: none !important; } #app-shell { max-width: var(--app-max-width) !important; margin: 0 auto !important; } #hero-card, #skill-card, #prompt-card, #output-card, #instructions-card, #tips-card { border: 1px solid var(--border) !important; border-radius: var(--radius-lg) !important; background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)) !important; box-shadow: var(--shadow-sm) !important; backdrop-filter: blur(14px); } #hero-card { padding: var(--space-6); margin-bottom: var(--space-4); } #hero-topline { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 10px; padding: 7px 12px; border-radius: 999px; background: rgba(255,255,255,0.05); border: 1px solid var(--border); color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 700; } #hero-topline::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: var(--accent); box-shadow: 0 0 0 6px rgba(18,185,129,0.12); } #hero-title { margin: 0; color: var(--text); font-size: clamp(1.55rem, 2.6vw, 2.35rem); font-weight: 780; line-height: 1.05; letter-spacing: -0.03em; } #hero-copy { margin: 10px 0 0 0; max-width: 70ch; color: var(--text-muted); font-size: 1rem; line-height: 1.65; } #layout-row { gap: var(--space-4); } #left-column, #right-column { gap: var(--space-4) !important; } #skill-card, #prompt-card, #output-card, #instructions-card, #tips-card { padding: var(--space-5); } #section-title { margin: 0 0 6px 0; color: var(--text); font-size: 1.02rem; font-weight: 700; letter-spacing: -0.02em; } #section-copy { margin: 0 0 var(--space-4) 0; color: var(--text-muted); font-size: 0.94rem; } #status-markdown { margin-top: var(--space-3); } .status-card { padding: 14px 15px; border-radius: var(--radius-md); background: var(--panel-3); border: 1px solid var(--border); } .status-card.ok { background: linear-gradient(180deg, rgba(18,185,129,0.10), rgba(16,25,43,0.96)); border-color: var(--accent-strong); } .status-card.warn { background: linear-gradient(180deg, rgba(245,158,11,0.10), rgba(16,25,43,0.96)); border-color: rgba(245,158,11,0.26); overflow-wrap: anywhere; } .status-grid { display: grid; gap: 10px; } .status-item { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; } .status-label { color: var(--text-faint); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.06em; } .status-value { color: var(--text); font-weight: 650; text-align: right; } .status-note { color: var(--text-muted); font-size: 0.93rem; line-height: 1.5; } textarea, input, select { font-size: 16px !important; } .gr-textbox textarea, .gr-textbox input, .gr-dropdown select { background: var(--panel-3) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: var(--radius-md) !important; box-shadow: none !important; } .gr-textbox textarea:focus, .gr-textbox input:focus, .gr-dropdown select:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 4px var(--accent-soft) !important; } label, .gr-block label, .gr-form label { color: var(--text) !important; font-weight: 650 !important; } .gr-form .hint, .gr-form .description, .gradio-container .prose, .gradio-container .gr-markdown p, .gradio-container .gr-markdown { color: var(--text-muted) !important; } #submit-btn button, #enable-btn button, #instructions-btn button, #refresh-btn button, #clear-btn button { min-height: 46px !important; border-radius: 14px !important; font-weight: 700 !important; border: 1px solid var(--border) !important; transition: transform 180ms ease, filter 180ms ease, background 180ms ease !important; } #submit-btn button { min-height: 54px !important; background: var(--accent) !important; color: white !important; border-color: transparent !important; box-shadow: 0 16px 34px rgba(18, 185, 129, 0.26) !important; } #submit-btn button:hover, #enable-btn button:hover, #refresh-btn button:hover, #instructions-btn button:hover, #clear-btn button:hover { transform: translateY(-1px); filter: brightness(1.03); } #enable-btn button { background: rgba(255,255,255,0.06) !important; color: var(--text) !important; } #refresh-btn button, #instructions-btn button, #clear-btn button { background: transparent !important; color: var(--text-muted) !important; } #output-box textarea, #instructions-box textarea { font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace !important; line-height: 1.6 !important; } #tips-list { color: var(--text-muted); margin: 0; } #tips-list li { margin: 0 0 10px 0; } .gr-accordion, .gr-accordion .label-wrap { border-radius: var(--radius-md) !important; } .gr-accordion { border: 1px solid var(--border) !important; background: rgba(255,255,255,0.02) !important; } .gr-accordion .label-wrap, .gr-accordion button { background: transparent !important; color: var(--text) !important; border: 0 !important; box-shadow: none !important; } code { background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 8px; } @media (max-width: 900px) { #hero-card, #skill-card, #prompt-card, #output-card, #instructions-card, #tips-card { padding: var(--space-4); } } #skill-dropdown { border-radius: var(--radius-md) !important; } #skill-dropdown input[role="combobox"] { background: var(--panel-3) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: var(--radius-md) !important; box-shadow: none !important; cursor: pointer !important; } #skill-dropdown input[role="combobox"]:focus { border-color: var(--accent) !important; box-shadow: 0 0 0 4px var(--accent-soft) !important; } """ def compress_skill_text(text, max_chars=MAX_SKILL_CHARS): """Drop stopwords from prose lines; keep code blocks and short lines intact.""" if len(text) <= max_chars: return text out = [] in_code = False for line in text.splitlines(): stripped = line.strip() if stripped.startswith("```"): in_code = not in_code out.append(line) continue if in_code or not stripped or stripped.startswith("#") or len(stripped) < 40: out.append(line) continue words = line.split() kept = [w for w in words if w.lower().strip(".,;:!?()[]") not in STOPWORDS] out.append(" ".join(kept)) return "\n".join(out)[:max_chars] def build_system_prompt(skill_name, skill_path, skill_text): return ( "You are an Ethereum-focused assistant using ETHSkills instructions. " "Follow the provided skill faithfully, but answer clearly and directly.\n\n" f"ACTIVE SKILL: {skill_name}\n" f"SKILL FILE: {skill_path}\n\n" "SKILL INSTRUCTIONS:\n" f"{compress_skill_text(skill_text)}" ) def discover_skills(): skills = {} if not SKILLS_ROOT.exists(): return skills root_skill = SKILLS_ROOT / "SKILL.md" if root_skill.is_file(): skills["ethskills-root"] = { "path": str(root_skill), "description": "Top-level ETHSkills router skill", } for item in sorted(SKILLS_ROOT.iterdir()): if item.is_dir(): skill_file = item / "SKILL.md" if skill_file.is_file(): skills[item.name] = { "path": str(skill_file), "description": f"ETHSkills skill: {item.name}", } return skills def load_state(): discovered = discover_skills() if STATE_FILE.exists(): with open(STATE_FILE, "r", encoding="utf-8") as f: saved = json.load(f) else: saved = {} merged = {} for name, meta in discovered.items(): merged[name] = { "installed": saved.get(name, {}).get("installed", False), "description": saved.get(name, {}).get("description", meta["description"]), "path": meta["path"], } return merged def save_state(state): with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, indent=2) def refresh_state(): state = load_state() save_state(state) return state def get_skill_choices(): return sorted(refresh_state().keys()) def normalize_skill_name(value): if isinstance(value, str): return value if isinstance(value, (list, tuple)) and value and isinstance(value[0], str): return value[0] return None def get_default_skill(): choices = get_skill_choices() for choice in choices: if isinstance(choice, str): return choice return None def get_internal_model_id(): response = requests.get(LLAMA_MODELS_URL, timeout=30) response.raise_for_status() data = response.json() return data["data"][0]["id"] def warm_skill(skill_name): """Preload the skill's system prompt into llama-server's KV cache.""" state = refresh_state() if skill_name not in state: return skill_path = state[skill_name]["path"] if not os.path.exists(skill_path): return with open(skill_path, "r", encoding="utf-8") as f: skill_text = f.read() try: requests.post( LLAMA_SERVER_URL, json={ "model": get_internal_model_id(), "messages": [ {"role": "system", "content": build_system_prompt(skill_name, skill_path, skill_text)}, {"role": "user", "content": "ok"}, ], "max_tokens": 4, "stream": False, }, timeout=600, ) except requests.RequestException: pass def render_status_card(skill_name): skill_name = normalize_skill_name(skill_name) state = refresh_state() if not state: return """
/app/skills/ethskills exists and contains one or more SKILL.md files.{meta.get('path', '')}Activate a skill, inspect the loaded instruction file, and route your prompt to the local Qwen-backed model with a cleaner, more production-ready workspace.
""" ) with gr.Row(equal_height=False, elem_id="layout-row"): with gr.Column(scale=4, min_width=320, elem_id="left-column"): with gr.Group(elem_id="skill-card"): gr.HTML( """Choose the active skill, refresh discovery, and enable it before running a prompt.
""" ) skill_name = gr.Dropdown( choices=get_skill_choices(), value=None, label="Active skill", info="Select which ETH skill should shape the system prompt.", interactive=True, filterable=False, allow_custom_value=False, elem_id="skill-dropdown", ) status = gr.HTML( value=render_status_card(None), elem_id="status-markdown", ) with gr.Row(equal_height=True): refresh_btn = gr.Button("Refresh", elem_id="refresh-btn") enable_btn = gr.Button("Enable", elem_id="enable-btn") instructions_btn = gr.Button("Open SKILL.md", elem_id="instructions-btn") with gr.Group(elem_id="tips-card"): gr.HTML( """A few small cues that help the flow feel fast and predictable.
SKILL.md stays secondary so the output panel remains the main workspace.This prompt goes through the cached skillset to the Qwen model.
""" ) user_input = gr.Textbox( label="Prompt", placeholder="Example: Explain the safest wallet flow for a new Ethereum app.", lines=8, ) with gr.Row(equal_height=True): clear_btn = gr.Button("Clear", elem_id="clear-btn") submit_btn = gr.Button("Submit", elem_id="submit-btn") with gr.Group(elem_id="output-card"): gr.HTML( """Model outputs an answer without the skill if it doesn't load in 3 minutes.
""" ) output = gr.Textbox( label="Response", lines=18, elem_id="output-box", ) with gr.Group(elem_id="instructions-card"): with gr.Accordion("Skill instructions", open=False): instructions_output = gr.Textbox( label="Selected SKILL.md", lines=18, elem_id="instructions-box", ) skill_name.change( fn=update_status_for_selection, inputs=skill_name, outputs=status, ) refresh_btn.click( fn=refresh_dropdown_and_status, outputs=[skill_name, status], ) enable_btn.click( fn=install_skill, inputs=skill_name, outputs=[output, status], ) instructions_btn.click( fn=read_skill, inputs=skill_name, outputs=instructions_output, ) clear_btn.click( fn=clear_prompt, outputs=user_input, ) submit_btn.click( fn=run_skill, inputs=[skill_name, user_input], outputs=output, ) demo.load( fn=refresh_dropdown_and_status, outputs=[skill_name, status], ) return demo api = FastAPI() @api.get("/v1/models") def list_models(): return { "object": "list", "data": [ { "id": PUBLIC_MODEL_ID, "object": "model", "owned_by": "jeeltcraft-luminous", } ], } @api.get("/health") def health(): try: response = requests.get("http://127.0.0.1:8080/health", timeout=5) try: content = response.json() except Exception: content = {"detail": response.text} return JSONResponse(status_code=response.status_code, content=content) except requests.RequestException as e: return JSONResponse( status_code=503, content={"status": "unavailable", "detail": str(e)}, ) @api.post("/v1/chat/completions") async def chat_completions(request: Request): try: body = await request.json() except Exception: raise HTTPException(status_code=400, detail="Invalid JSON body") body["model"] = get_internal_model_id() try: response = requests.post(LLAMA_SERVER_URL, json=body, timeout=600) except requests.exceptions.Timeout: return JSONResponse( status_code=504, content={"error": {"message": "Backend inference timed out. Try a shorter prompt or retry.", "type": "timeout_error"}}, ) except requests.exceptions.ConnectionError: return JSONResponse( status_code=503, content={"error": {"message": "Backend is warming up — retry in 30 seconds.", "type": "connection_error"}}, ) try: content = response.json() except Exception: content = {"detail": response.text} return JSONResponse(status_code=response.status_code, content=content) demo = create_demo() app = mount_gradio_app(api, demo, path="/") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))