Spaces:
Runtime error
Runtime error
File size: 12,982 Bytes
0ed8d20 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | """
Mini Coding Agent — Gradio frontend for Hugging Face Spaces.
Architecture:
Browser → Gradio (this file, Python) → Elixir backend (HTTP) → LLM provider
↘ (or directly to LLM if backend is offline)
The Elixir backend (see elixir_backend/) handles the actual agent loop:
prompt construction, multi-file project planning, and provider dispatch.
This file is purely UI + a thin HTTP client.
"""
from __future__ import annotations
import io
import json
import os
import re
import time
import zipfile
from typing import Iterator
import gradio as gr
import requests
# --------------------------------------------------------------------------- #
# Config
# --------------------------------------------------------------------------- #
ELIXIR_BACKEND_URL = os.getenv("ELIXIR_BACKEND_URL", "http://localhost:4000")
REQUEST_TIMEOUT = 180
PROVIDERS = {
"OpenAI (gpt-4o-mini)": {"id": "openai", "model": "gpt-4o-mini",
"key_hint": "sk-...", "url": "https://platform.openai.com/api-keys"},
"Anthropic (claude-3-5-sonnet)": {"id": "anthropic", "model": "claude-3-5-sonnet-latest",
"key_hint": "sk-ant-...", "url": "https://console.anthropic.com/settings/keys"},
"Groq (llama-3.3-70b, free)": {"id": "groq", "model": "llama-3.3-70b-versatile",
"key_hint": "gsk_...", "url": "https://console.groq.com/keys"},
}
SYSTEM_PROMPT = """You are a senior software engineer acting as a project scaffolding agent.
When the user describes a project, output a complete, runnable codebase as multiple files.
OUTPUT FORMAT — strictly follow this:
For each file, emit a fenced code block whose info string is the file path, e.g.:
```python:src/main.py
# code here
```
```text:README.md
# Project
```
Rules:
- Use the language identifier before the colon (python, javascript, typescript, html, css, json, text, etc.).
- The path AFTER the colon is the file path relative to the project root.
- Include a README.md and any config files (requirements.txt, package.json, etc.) needed to run the project.
- Keep files focused and small. Prefer 3–8 files over one giant file.
- After the files, add a short "How to run" section in plain prose.
"""
# --------------------------------------------------------------------------- #
# Backend client
# --------------------------------------------------------------------------- #
def call_elixir_backend(provider: str, model: str, api_key: str,
history: list[dict]) -> Iterator[str]:
"""Stream tokens from the Elixir backend. Falls back to direct provider call."""
try:
with requests.post(
f"{ELIXIR_BACKEND_URL}/api/agent/stream",
json={"provider": provider, "model": model, "api_key": api_key,
"system": SYSTEM_PROMPT, "messages": history},
stream=True, timeout=REQUEST_TIMEOUT,
) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
payload = line[6:]
if payload == "[DONE]":
return
try:
chunk = json.loads(payload)
except json.JSONDecodeError:
continue
if "delta" in chunk:
yield chunk["delta"]
elif "error" in chunk:
yield f"\n\n**Backend error:** {chunk['error']}"
return
except requests.exceptions.RequestException as e:
yield f"⚠️ Elixir backend unreachable ({e}). Falling back to direct call.\n\n"
yield from call_provider_direct(provider, model, api_key, history)
def call_provider_direct(provider: str, model: str, api_key: str,
history: list[dict]) -> Iterator[str]:
"""Fallback: call the LLM provider directly from Python (no streaming, simpler)."""
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history
if provider in ("openai", "groq"):
url = ("https://api.openai.com/v1/chat/completions" if provider == "openai"
else "https://api.groq.com/openai/v1/chat/completions")
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
body = {"model": model, "messages": messages, "stream": True}
try:
with requests.post(url, headers=headers, json=body, stream=True,
timeout=REQUEST_TIMEOUT) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
data = line[6:]
if data == "[DONE]":
return
try:
delta = json.loads(data)["choices"][0]["delta"].get("content", "")
except (json.JSONDecodeError, KeyError, IndexError):
continue
if delta:
yield delta
except requests.exceptions.HTTPError as e:
yield f"\n\n**Provider error:** {e.response.status_code} {e.response.text[:300]}"
except Exception as e:
yield f"\n\n**Error:** {e}"
elif provider == "anthropic":
url = "https://api.anthropic.com/v1/messages"
headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01",
"Content-Type": "application/json"}
body = {"model": model, "max_tokens": 4096, "system": SYSTEM_PROMPT,
"messages": history, "stream": True}
try:
with requests.post(url, headers=headers, json=body, stream=True,
timeout=REQUEST_TIMEOUT) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
try:
evt = json.loads(line[6:])
except json.JSONDecodeError:
continue
if evt.get("type") == "content_block_delta":
delta = evt.get("delta", {}).get("text", "")
if delta:
yield delta
except requests.exceptions.HTTPError as e:
yield f"\n\n**Provider error:** {e.response.status_code} {e.response.text[:300]}"
except Exception as e:
yield f"\n\n**Error:** {e}"
# --------------------------------------------------------------------------- #
# File extraction
# --------------------------------------------------------------------------- #
FILE_BLOCK_RE = re.compile(
r"```([a-zA-Z0-9_+\-]*?):([^\n`]+)\n(.*?)```",
re.DOTALL,
)
def extract_files(markdown: str) -> dict[str, str]:
"""Pull `lang:path\\n...` fenced blocks into a {path: content} dict."""
files: dict[str, str] = {}
for _lang, path, body in FILE_BLOCK_RE.findall(markdown):
path = path.strip()
if path:
files[path] = body.rstrip() + "\n"
return files
def make_zip(files: dict[str, str]) -> str | None:
if not files:
return None
path = "/tmp/generated_project.zip"
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
for name, content in files.items():
zf.writestr(name, content)
return path
# --------------------------------------------------------------------------- #
# Gradio handlers
# --------------------------------------------------------------------------- #
def chat_fn(message: str, history: list[dict], provider_label: str, api_key: str):
if not api_key.strip():
yield history + [
{"role": "user", "content": message},
{"role": "assistant", "content": "🔑 Please paste your API key in the panel on the left first."},
], gr.update(), gr.update(visible=False)
return
if not message.strip():
yield history, gr.update(), gr.update(visible=False)
return
cfg = PROVIDERS[provider_label]
convo = history + [{"role": "user", "content": message}]
assistant_buf = ""
yield convo + [{"role": "assistant", "content": "▍"}], gr.update(), gr.update(visible=False)
for tok in call_elixir_backend(cfg["id"], cfg["model"], api_key.strip(), convo):
assistant_buf += tok
yield convo + [{"role": "assistant", "content": assistant_buf + "▍"}], gr.update(), gr.update(visible=False)
time.sleep(0) # cooperative yield
final = convo + [{"role": "assistant", "content": assistant_buf}]
files = extract_files(assistant_buf)
zip_path = make_zip(files)
file_summary = ""
if files:
file_summary = "### 📁 Generated files\n" + "\n".join(f"- `{p}`" for p in files)
yield final, file_summary, gr.update(value=zip_path, visible=bool(zip_path))
def clear_chat():
return [], "", gr.update(visible=False)
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
CUSTOM_CSS = """
.gradio-container { max-width: 1280px !important; }
#title { text-align: center; padding: 8px 0 4px; }
#title h1 { background: linear-gradient(90deg,#7c3aed,#06b6d4); -webkit-background-clip: text;
-webkit-text-fill-color: transparent; font-size: 2.2rem; margin: 0; }
#subtitle { text-align: center; color: #94a3b8; margin-bottom: 12px; }
.api-panel { border: 1px solid rgba(124,58,237,.25); border-radius: 14px; padding: 14px;
background: linear-gradient(180deg, rgba(124,58,237,.05), rgba(6,182,212,.03)); }
footer { display: none !important; }
"""
with gr.Blocks(theme=gr.themes.Soft(primary_hue="violet", secondary_hue="cyan"),
css=CUSTOM_CSS, title="Mini Coding Agent") as demo:
gr.HTML('<div id="title"><h1>⚡ Mini Coding Agent</h1></div>'
'<div id="subtitle">Multi-file project scaffolding · Python frontend · Elixir backend</div>')
with gr.Row():
with gr.Column(scale=1, min_width=280):
with gr.Group(elem_classes="api-panel"):
gr.Markdown("### 🔐 Provider")
provider = gr.Dropdown(
choices=list(PROVIDERS.keys()),
value="Groq (llama-3.3-70b, free)",
label="Model provider",
info="Groq is free & fast — great for demos.",
)
api_key = gr.Textbox(
label="API key",
placeholder="Paste your key…",
type="password",
info="Stays in your browser session. Never stored.",
)
key_links = gr.Markdown(
"\n".join(f"- [{name}]({cfg['url']})" for name, cfg in PROVIDERS.items())
)
gr.Markdown("### 💡 Try")
gr.Examples(
examples=[
"Build a Flask todo API with SQLite, including tests.",
"Create a small React + Vite landing page for a coffee shop.",
"Write a Python CLI that scrapes Hacker News front page to JSON.",
"Scaffold a FastAPI app with JWT auth and a users endpoint.",
],
inputs=None, # filled below
label=None,
)
with gr.Column(scale=3):
chatbot = gr.Chatbot(
type="messages", height=540, show_label=False,
avatar_images=(None, "https://api.dicebear.com/7.x/bottts/svg?seed=agent"),
render_markdown=True,
)
with gr.Row():
msg = gr.Textbox(placeholder="Describe the project you want to build…",
show_label=False, scale=8, container=False)
send = gr.Button("Send", variant="primary", scale=1)
clear = gr.Button("Clear", scale=1)
files_md = gr.Markdown("")
zip_dl = gr.File(label="Download project (.zip)", visible=False)
# Wire examples now that `msg` exists
demo.load(lambda: None) # no-op; placeholder
send.click(chat_fn, [msg, chatbot, provider, api_key],
[chatbot, files_md, zip_dl]).then(lambda: "", None, msg)
msg.submit(chat_fn, [msg, chatbot, provider, api_key],
[chatbot, files_md, zip_dl]).then(lambda: "", None, msg)
clear.click(clear_chat, None, [chatbot, files_md, zip_dl])
if __name__ == "__main__":
demo.queue(default_concurrency_limit=4).launch(
server_name="0.0.0.0",
server_port=int(os.getenv("PORT", 7860)),
)
|