Luminous / app.py
jeeltcraft's picture
Update app.py
39b00ea verified
Raw
History Blame Contribute Delete
27.6 kB
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()
@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)))