"""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)