autoapp-builder / app /engine /app_planner.py
ruslanmv's picture
Wire to real HF LLM plan + code generation
a30d381 verified
Raw
History Blame Contribute Delete
4.81 kB
"""Plan an application from a prompt using a real Hugging Face LLM.
Returns a structured plan including the actual source files for a deployable
Hugging Face Space. Falls back to a deterministic heuristic plan if the LLM is
unavailable (so the builder never hard-fails).
"""
from __future__ import annotations
import json
import re
from app.inference import InferenceError, chat
SYSTEM_PROMPT = (
"You are a senior engineer that scaffolds complete, deployable Hugging Face "
"Spaces from a product description. You output STRICT JSON only — no prose, "
"no markdown fences."
)
USER_TEMPLATE = """Design a deployable Hugging Face Space for this idea:
\"\"\"{prompt}\"\"\"
Preferred SDK: {preferred_sdk} (if "auto", pick the best of: gradio, docker, static)
Return STRICT JSON with EXACTLY this shape:
{{
"app_name": "kebab-case-name",
"selected_sdk": "gradio|docker|static",
"summary": "2-3 sentence description of what the app does",
"features": ["feature 1", "feature 2", "feature 3"],
"files": [
{{"path": "README.md", "content": "...full file content..."}},
{{"path": "app.py", "content": "..."}},
{{"path": "requirements.txt", "content": "..."}}
]
}}
Requirements for the generated files:
- Include a README.md whose top is a YAML metadata block (---) with title, emoji,
colorFrom, colorTo, sdk, and (app_file for gradio/static, or app_port: 7860 for docker).
- For gradio: provide app.py (a runnable gradio app) and requirements.txt.
- For docker: provide Dockerfile exposing 7860, app.py, requirements.txt.
- For static: provide index.html.
- Code must be COMPLETE and runnable, not pseudo-code. Keep it focused and self-contained.
- Prefer free Hugging Face inference (huggingface_hub InferenceClient) when the app needs AI.
- Output JSON only.
"""
def _slugify(text: str) -> str:
text = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()).strip("-")
return text[:50] or "generated-space"
def _extract_json(text: str) -> dict:
text = text.strip()
text = re.sub(r"^```(?:json)?", "", text).strip()
text = re.sub(r"```$", "", text).strip()
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1:
raise ValueError("No JSON object in model output.")
return json.loads(text[start : end + 1])
def _heuristic_plan(prompt: str, preferred_sdk: str) -> dict:
prompt_lower = prompt.lower()
selected_sdk = preferred_sdk
if preferred_sdk == "auto":
if any(k in prompt_lower for k in ["stream", "agent", "api", "sandbox", "workflow"]):
selected_sdk = "docker"
elif any(k in prompt_lower for k in ["landing page", "portfolio", "static"]):
selected_sdk = "static"
else:
selected_sdk = "gradio"
return {
"prompt": prompt,
"app_name": _slugify(prompt.split(".")[0][:60]),
"selected_sdk": selected_sdk,
"summary": f"Starter scaffold for: {prompt}",
"features": [
"configurable UI scaffold",
"placeholder business logic",
"Hugging Face Space metadata",
"local development instructions",
],
"files": [], # signals repo_generator to use built-in templates
"source": "fallback",
}
_VALID_SDKS = {"gradio", "docker", "static"}
def plan_application(prompt: str, preferred_sdk: str = "auto") -> dict:
if not prompt or not prompt.strip():
return _heuristic_plan("Untitled app", preferred_sdk)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_TEMPLATE.format(prompt=prompt, preferred_sdk=preferred_sdk)},
]
try:
raw = chat(messages, max_tokens=2600, temperature=0.4)
data = _extract_json(raw)
sdk = str(data.get("selected_sdk", "gradio")).lower().strip()
if preferred_sdk in _VALID_SDKS:
sdk = preferred_sdk
if sdk not in _VALID_SDKS:
sdk = "gradio"
files = []
for f in data.get("files", []) or []:
path = str(f.get("path", "")).strip()
content = f.get("content", "")
if path and isinstance(content, str) and content.strip():
files.append({"path": path, "content": content})
return {
"prompt": prompt,
"app_name": _slugify(str(data.get("app_name") or prompt.split(".")[0])),
"selected_sdk": sdk,
"summary": str(data.get("summary", "")).strip() or f"Generated app for: {prompt}",
"features": [str(x) for x in (data.get("features") or [])][:8],
"files": files,
"source": "llm",
}
except InferenceError:
raise
except Exception:
return _heuristic_plan(prompt, preferred_sdk)