Spaces:
Running
Running
| 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 """ | |
| <div class='status-card warn'> | |
| <div class='status-grid'> | |
| <div class='status-item'><span class='status-label'>Status</span><span class='status-value'>No skills found</span></div> | |
| <div class='status-note'>Check that <code>/app/skills/ethskills</code> exists and contains one or more <code>SKILL.md</code> files.</div> | |
| </div> | |
| </div> | |
| """ | |
| installed_count = sum(1 for x in state.values() if x.get("installed")) | |
| if not skill_name or skill_name not in state: | |
| return f""" | |
| <div class='status-card'> | |
| <div class='status-grid'> | |
| <div class='status-item'><span class='status-label'>Discovered</span><span class='status-value'>{len(state)} skills</span></div> | |
| <div class='status-item'><span class='status-label'>Enabled</span><span class='status-value'>{installed_count}</span></div> | |
| <div class='status-note'>Select a skill to inspect its availability and load its instructions.</div> | |
| </div> | |
| </div> | |
| """ | |
| meta = state[skill_name] | |
| enabled = bool(meta.get("installed")) | |
| tone = "ok" if enabled else "warn" | |
| enabled_text = "Enabled" if enabled else "Not enabled" | |
| return f""" | |
| <div class='status-card {tone}'> | |
| <div class='status-grid'> | |
| <div class='status-item'><span class='status-label'>Selected</span><span class='status-value'>{skill_name}</span></div> | |
| <div class='status-item'><span class='status-label'>State</span><span class='status-value'>{enabled_text}</span></div> | |
| <div class='status-item'><span class='status-label'>Path</span><span class='status-value'><code>{meta.get('path', '')}</code></span></div> | |
| <div class='status-note'>{meta.get('description', '')}</div> | |
| </div> | |
| </div> | |
| """ | |
| def refresh_dropdown_and_status(): | |
| choices = get_skill_choices() | |
| value = choices[0] if choices else None | |
| return gr.update(choices=choices, value=value), render_status_card(value) | |
| def update_status_for_selection(skill_name): | |
| return render_status_card(skill_name) | |
| def install_skill(skill_name): | |
| skill_name = normalize_skill_name(skill_name) | |
| state = refresh_state() | |
| if skill_name not in state: | |
| return "Skill not found.", render_status_card(None) | |
| state[skill_name]["installed"] = True | |
| save_state(state) | |
| threading.Thread(target=warm_skill, args=(skill_name,), daemon=True).start() | |
| return f"{skill_name} enabled.", render_status_card(skill_name) | |
| def read_skill(skill_name): | |
| skill_name = normalize_skill_name(skill_name) | |
| state = refresh_state() | |
| if skill_name not in state: | |
| return "Skill not found." | |
| skill_path = state[skill_name]["path"] | |
| if not os.path.exists(skill_path): | |
| return f"Skill file missing: {skill_path}" | |
| with open(skill_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| def call_llama(system_prompt, user_prompt, timeout=180): | |
| messages = [] | |
| if system_prompt: | |
| messages.append({"role": "system", "content": system_prompt}) | |
| messages.append({"role": "user", "content": user_prompt}) | |
| payload = { | |
| "model": get_internal_model_id(), | |
| "messages": messages, | |
| "temperature": 0.2, | |
| "stream": False, | |
| "max_tokens": 600, | |
| } | |
| response = requests.post(LLAMA_SERVER_URL, json=payload, timeout=timeout) | |
| response.raise_for_status() | |
| data = response.json() | |
| choice = data["choices"][0] | |
| text = choice["message"]["content"] | |
| if choice.get("finish_reason") == "length": | |
| text += "\n\n[Output truncated — increase max_tokens if needed]" | |
| return text | |
| def run_skill(skill_name, user_input): | |
| skill_name = normalize_skill_name(skill_name) | |
| state = refresh_state() | |
| if skill_name not in state: | |
| return "Skill not found." | |
| if not state[skill_name].get("installed"): | |
| return f"{skill_name} is not enabled. Tap 'Enable' first." | |
| prompt = (user_input or "").strip() | |
| if not prompt: | |
| return "Please enter a prompt." | |
| skill_path = state[skill_name]["path"] | |
| if not os.path.exists(skill_path): | |
| return f"Skill file missing: {skill_path}" | |
| with open(skill_path, "r", encoding="utf-8") as f: | |
| skill_text = f.read() | |
| system_prompt = build_system_prompt(skill_name, skill_path, skill_text) | |
| try: | |
| return call_llama(system_prompt, prompt, timeout=120) | |
| except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): | |
| pass | |
| except requests.exceptions.RequestException as e: | |
| return f"Failed to call llama-server: {str(e)}" | |
| except KeyError: | |
| return "llama-server returned an unexpected response format." | |
| # Fallback: skill prompt timed out, retry with a minimal system prompt | |
| fallback_prompt = ( | |
| "You are an Ethereum-focused assistant. Answer clearly and directly." | |
| ) | |
| try: | |
| answer = call_llama(fallback_prompt, prompt, timeout=600) | |
| return ( | |
| "[Note: the skill instructions timed out on the CPU backend, so this " | |
| "answer was generated without them.]\n\n" + answer | |
| ) | |
| except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): | |
| # Last resort: no system prompt at all | |
| try: | |
| answer = call_llama(None, prompt, timeout=600) | |
| return ( | |
| "[Note: the model timed out with instructions, so this answer " | |
| "was generated with a plain prompt.]\n\n" + answer | |
| ) | |
| except requests.exceptions.RequestException as e: | |
| return f"The model is overloaded right now — try again in a minute. ({str(e)})" | |
| except requests.exceptions.RequestException as e: | |
| return f"Failed to call llama-server: {str(e)}" | |
| def clear_prompt(): | |
| return "" | |
| def create_demo(): | |
| theme = gr.themes.Soft( | |
| primary_hue="emerald", | |
| secondary_hue="slate", | |
| neutral_hue="slate", | |
| ) | |
| with gr.Blocks(theme=theme, fill_width=True, css=CSS, title="ETH Skills Interface") as demo: | |
| with gr.Column(elem_id="app-shell"): | |
| with gr.Group(elem_id="hero-card"): | |
| gr.HTML( | |
| """ | |
| <div id='hero-topline'>Luminous Coder Model</div> | |
| <h1 id='hero-title'>ETH Skills Interface</h1> | |
| <p id='hero-copy'> | |
| 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. | |
| </p> | |
| """ | |
| ) | |
| 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( | |
| """ | |
| <h2 id='section-title'>Skill control</h2> | |
| <p id='section-copy'> | |
| Choose the active skill, refresh discovery, and enable it before running a prompt. | |
| </p> | |
| """ | |
| ) | |
| 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( | |
| """ | |
| <h2 id='section-title'>Usage notes</h2> | |
| <p id='section-copy'> | |
| A few small cues that help the flow feel fast and predictable. | |
| </p> | |
| <ul id='tips-list'> | |
| <li>Enable a skill once, then reuse it across prompts until the state file changes.</li> | |
| <li>The selected <code>SKILL.md</code> stays secondary so the output panel remains the main workspace.</li> | |
| <li>Prompt and response areas are sized for long-form debugging, strategy notes, and protocol explanations.</li> | |
| </ul> | |
| """ | |
| ) | |
| with gr.Column(scale=8, min_width=480, elem_id="right-column"): | |
| with gr.Group(elem_id="prompt-card"): | |
| gr.HTML( | |
| """ | |
| <h2 id='section-title'>Prompt</h2> | |
| <p id='section-copy'> | |
| This prompt goes through the cached skillset to the Qwen model. | |
| </p> | |
| """ | |
| ) | |
| 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( | |
| """ | |
| <h2 id='section-title'>Response</h2> | |
| <p id='section-copy'> | |
| Model outputs an answer without the skill if it doesn't load in 3 minutes. | |
| </p> | |
| """ | |
| ) | |
| 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() | |
| def list_models(): | |
| return { | |
| "object": "list", | |
| "data": [ | |
| { | |
| "id": PUBLIC_MODEL_ID, | |
| "object": "model", | |
| "owned_by": "jeeltcraft-luminous", | |
| } | |
| ], | |
| } | |
| 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)}, | |
| ) | |
| 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))) |