| """ |
| HuggingFace Spaces entry point for the Self-Healing Code Agent. |
| |
| WHY THIS FILE EXISTS |
| -------------------- |
| HuggingFace Spaces follows a strict convention: it expects a file named ``app.py`` |
| at the *repository root* and runs it with ``python app.py`` when the Space starts. |
| Everything that should run on Space startup lives here: |
| |
| 1. Pre-warming the local generator model so the first user request is fast. |
| 2. Constructing the AgentConfig and LLMRouter that Gradio will share across |
| every request. |
| 3. Building the Gradio Blocks app and launching the HTTP server on |
| ``0.0.0.0:$PORT`` (HF Spaces expects port 7860 by default). |
| |
| WHY THE ROUTING IS HYBRID (HF + CLAUDE) |
| --------------------------------------- |
| The agent is *intentionally* designed so the **generator** role (initial code |
| writing) uses a small, weak model (Llama-3.2-3B). A weak generator is what makes |
| the self-healing loop interesting β strong models like Claude one-shot most |
| tasks, so you never see the diagnose β repair cycle that the project is meant to |
| showcase. The other roles (QA adversarial tester, debugger, critic, memory |
| summarizer) all use Claude (Anthropic API) for higher reasoning quality. |
| |
| This split is wired in :func:`llm.router.build_router_with_generator_override` |
| and the conditions there determine which provider gets bound to the |
| ``generator`` role. To enable the small-model generator on HF Spaces you MUST |
| set ``LLM_PROVIDER=huggingface`` as a *Variable* in the Space settings; |
| otherwise the generator silently falls back to Claude and the repair loop |
| almost never triggers. |
| |
| WHY autonomy_level IS OVERRIDDEN TO full_auto |
| --------------------------------------------- |
| :class:`AgentConfig` defaults to ``autonomy_level="review_repairs"`` which uses |
| LangGraph's ``interrupt()`` to pause the graph after every diagnosis so a human |
| can approve each repair. That is the right default for local development but it |
| is *not* reliable inside the Gradio UI on HF Spaces (the resume-from-pause flow |
| can desync with the streaming generator), so we explicitly force ``full_auto`` |
| here for the deployed Space. |
| |
| REQUIRED ENV VARS ON THE SPACE |
| ------------------------------ |
| - ``ANTHROPIC_API_KEY`` β Secret. Required for QA / debugger / critic / |
| memory-summarizer roles. |
| - ``LLM_PROVIDER=huggingface`` β Variable. Required to enable the local 3B |
| generator (otherwise generator β Claude). |
| |
| OPTIONAL ENV VARS |
| ----------------- |
| - ``HF_MODEL`` β Override the generator model id (default: |
| ``meta-llama/Llama-3.2-3B-Instruct``). |
| - ``HF_TOKEN`` β Required only if you want cross-session memory |
| to persist to a HuggingFace Dataset. |
| - ``PORT`` β Override the Gradio HTTP port (default 7860). |
| """ |
|
|
| import asyncio |
| import logging |
| import os |
| import sys |
| from pathlib import Path |
|
|
| |
| |
| |
| |
| sys.path.insert(0, str(Path(__file__).parent)) |
|
|
| |
| |
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| |
| |
| _PREWARM_ERROR: str | None = None |
|
|
|
|
| def _prewarm() -> None: |
| """Load the generator model into memory before Gradio starts accepting requests. |
| |
| Pre-warming is only meaningful for the HuggingFace provider β Claude and |
| Ollama don't load model weights into our process so there's nothing to |
| pre-load. We deliberately call ``asyncio.run()`` here because this runs at |
| import time, before any event loop is started by Gradio. Doing it now |
| avoids a 30-90s "first request takes forever" surprise for the first user. |
| |
| Pre-warm failures are caught and logged but never raised β a broken model |
| download should not prevent the Space from booting (the Performance tab and |
| the rest of the UI still work). |
| """ |
| global _PREWARM_ERROR |
|
|
| |
| |
| llm_provider = os.environ.get("LLM_PROVIDER", "").lower() |
|
|
| if llm_provider != "huggingface": |
| |
| |
| logger.info( |
| "LLM_PROVIDER=%r β not 'huggingface', skipping generator pre-warm.", |
| llm_provider or "(unset)", |
| ) |
| return |
|
|
| try: |
| |
| |
| from llm.router import build_router_with_generator_override |
|
|
| router = build_router_with_generator_override() |
|
|
| |
| |
| |
| |
| generator_provider = router._role_providers.get("generator") or router.provider |
|
|
| |
| |
| if hasattr(generator_provider, "_ensure_loaded"): |
| logger.info( |
| "Pre-warming generator model %s (this can take 30-90s on free CPU)...", |
| generator_provider.model_name, |
| ) |
| asyncio.run(generator_provider._ensure_loaded()) |
| logger.info("Pre-warm complete: %s", generator_provider.model_name) |
| else: |
| logger.info( |
| "Generator provider %r does not require pre-warming.", |
| generator_provider.provider_name, |
| ) |
| except Exception as exc: |
| |
| |
| |
| |
| _PREWARM_ERROR = f"{type(exc).__name__}: {exc}" |
| logger.error("Pre-warm failed (non-fatal, server still starting): %s", _PREWARM_ERROR) |
|
|
|
|
| |
| |
| |
| _prewarm() |
|
|
|
|
| |
| |
| |
| |
| from agent.config import AgentConfig |
| from demo.app import build_app |
| from llm.router import build_router_with_generator_override |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| _config = AgentConfig(autonomy_level="full_auto") |
| logger.info("Agent config: %s", _config) |
|
|
|
|
| |
| |
| |
| _router = build_router_with_generator_override() |
|
|
| |
| |
| |
| |
| _generator_provider = _router._role_providers.get("generator") or _router.provider |
| logger.info( |
| "Provider routing: generator=%s/%s | other roles=%s/%s", |
| _generator_provider.provider_name, |
| _generator_provider.model_name, |
| _router.provider.provider_name, |
| _router.provider.model_name, |
| ) |
| if _PREWARM_ERROR: |
| logger.error( |
| "STARTUP WARNING β generator pre-warm failed earlier: %s. The first generator " |
| "call will retry the load lazily; expect a long delay or failure on first request.", |
| _PREWARM_ERROR, |
| ) |
|
|
|
|
| |
| |
| |
| demo = build_app(config=_config, router=_router) |
|
|
| |
| |
| |
| |
| demo.queue() |
|
|
| |
| |
| |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=int(os.environ.get("PORT", 7860)), |
| ) |
|
|