Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import logging | |
| from typing import Generator | |
| logger = logging.getLogger(__name__) | |
| def _ev(event: str, **kw) -> dict: | |
| return {"event": event, **kw} | |
| def stream_turn(user_message: str, state: dict) -> Generator[dict, None, None]: | |
| """ | |
| Streaming agent turn. Yields event dicts consumed by app.py on_chat(). | |
| Event types: | |
| {"event": "step", "step": str, "label": str} | |
| {"event": "thinking", "text": str} | |
| {"event": "result", "state": dict, "reply": str, "code": str, "spec": dict|None} | |
| {"event": "error", "state": dict, "message": str} | |
| """ | |
| import copy | |
| from agent.intent import classify | |
| from agent.healer import heal | |
| from agent.memory import append_user, append_assistant | |
| from agent.patcher import patch as patch_spec | |
| from core.templates import render_page, render_app | |
| from core.sandbox import safe_exec | |
| from core.validator import validate_page_spec | |
| from storage.db import save_component | |
| from core.generator import MODEL_ID, get_device, _generate_messages, parse_json, _split_thinking | |
| from agent.planner import _load_agent_system, _ensure_page_schema | |
| state = copy.deepcopy(state) | |
| state = append_user(state, user_message) | |
| current_spec = state.get("current_spec") | |
| messages = state["messages"] | |
| # ββ classify βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| yield _ev("step", step="classifying", label="classifying intentβ¦") | |
| intent = classify(user_message, current_spec) | |
| logger.info("intent=%s", intent) | |
| # ββ explain shortcut ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent == "EXPLAIN": | |
| yield _ev("step", step="planning", label="generating explanationβ¦") | |
| reply = _explain(user_message, current_spec, messages) | |
| state = append_assistant(state, reply) | |
| state["_reply"] = reply | |
| yield _ev("result", state=state, reply=reply, code="", spec=current_spec) | |
| return | |
| # ββ plan or patch βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if intent in ("NEW",) or current_spec is None: | |
| step_label = "planning pageβ¦" | |
| elif intent == "RESTYLE": | |
| step_label = "restyling CSSβ¦" | |
| else: | |
| step_label = f"patching page ({intent})β¦" | |
| yield _ev("step", step="planning", label=step_label) | |
| try: | |
| if intent == "NEW" or current_spec is None: | |
| system = _load_agent_system() | |
| history = [m for m in messages if m["role"] != "system"] | |
| prompt_msgs = ( | |
| [{"role": "system", "content": system}] | |
| + history | |
| + [{"role": "user", "content": user_message}] | |
| ) | |
| raw = _generate_messages(prompt_msgs, max_tokens=2048) | |
| thinking, json_raw = _split_thinking(raw) | |
| if not json_raw.strip(): | |
| json_raw = raw | |
| if thinking: | |
| yield _ev("thinking", text=thinking) | |
| spec = _ensure_page_schema(parse_json(json_raw)) | |
| else: | |
| # EDIT / ADD / REMOVE / REPLACE / WIRE / FIX / RESTYLE | |
| spec = patch_spec(current_spec, user_message, intent, messages) | |
| except Exception as exc: | |
| logger.exception("plan/patch failed") | |
| err_msg = f"I couldn't understand that request ({exc}). Try rephrasing." | |
| state = _fail(state, err_msg) | |
| yield _ev("error", state=state, message=err_msg) | |
| return | |
| # ββ validate page spec ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ok_spec, val_errors = validate_page_spec(spec) | |
| if not ok_spec: | |
| logger.warning("Page spec validation errors: %s", val_errors) | |
| # non-fatal β still attempt render; healer may fix exec errors | |
| # ββ render ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| yield _ev("step", step="rendering", label="rendering pageβ¦") | |
| try: | |
| code = render_page(spec) | |
| except Exception as exc: | |
| logger.exception("render_page failed") | |
| # Try legacy render_app as fallback | |
| try: | |
| code = render_app(spec) | |
| except Exception as exc2: | |
| err_msg = f"Render error: {exc2}" | |
| state = _fail(state, err_msg) | |
| yield _ev("error", state=state, message=err_msg) | |
| return | |
| # ββ sandbox βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| yield _ev("step", step="validating", label="validating in sandboxβ¦") | |
| exec_ok, exec_err = safe_exec(code) | |
| # ββ heal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| heal_count = 0 | |
| if not exec_ok: | |
| yield _ev("step", step="healing", label="auto-healing errorβ¦") | |
| healed_code, healed_ok = heal(code, exec_err, messages, spec=spec) | |
| heal_count = 1 | |
| if healed_ok: | |
| code = healed_code | |
| exec_ok = True | |
| else: | |
| _trace(user_message, code, spec, False, False, exec_err, get_device(), MODEL_ID) | |
| err_msg = _friendly_error(exec_err) | |
| state = _fail(state, err_msg, spec=spec, code=code) | |
| yield _ev("error", state=state, message=err_msg) | |
| return | |
| # ββ save ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| yield _ev("step", step="saving", label="savingβ¦") | |
| try: | |
| save_component(user_message, code, spec) | |
| except Exception: | |
| logger.exception("save_component failed") | |
| _trace(user_message, code, spec, ok_spec, True, None, get_device(), MODEL_ID) | |
| # ββ commit ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| page = spec.get("page", spec) | |
| reasoning = spec.get("reasoning", "") | |
| reply = _format_reply(page, reasoning, intent) | |
| state = append_assistant(state, json.dumps(spec)) | |
| state["current_spec"] = spec | |
| state["current_code"] = code | |
| state["iteration"] += 1 | |
| state["heal_attempts"] = heal_count | |
| state["last_error"] = None | |
| state["_reply"] = reply | |
| yield _ev("result", state=state, reply=reply, code=code, spec=spec) | |
| def run_turn(user_message: str, state: dict) -> dict: | |
| """Non-streaming wrapper β consumes stream_turn and returns final state.""" | |
| final_state = state | |
| for event in stream_turn(user_message, state): | |
| if event["event"] in ("result", "error"): | |
| final_state = event["state"] | |
| return final_state | |
| def _explain(user_message: str, current_spec, messages: list[dict]) -> str: | |
| if current_spec is None: | |
| return "There's no page built yet. Describe an app and I'll build it." | |
| try: | |
| from agent.planner import _call_model, _load_agent_system | |
| system = _load_agent_system() | |
| prompt = ( | |
| f"The current page spec is:\n{json.dumps(current_spec, indent=2)}\n\n" | |
| f"User question: {user_message}\n\n" | |
| "Answer clearly and concisely. No JSON output needed." | |
| ) | |
| return _call_model( | |
| [{"role": "system", "content": system}, {"role": "user", "content": prompt}] | |
| ) | |
| except Exception as exc: | |
| return f"I can see the current page but couldn't generate an explanation ({exc})." | |
| def _format_reply(page: dict, reasoning: str, intent: str = "") -> str: | |
| sections = page.get("sections", []) | |
| title = page.get("title", "App") | |
| layout = page.get("layout", "") | |
| n_sections = len(sections) | |
| n_comps = sum(len(s.get("components", [])) for s in sections) | |
| action = intent.lower() or "built" | |
| lines = [ | |
| f"**{action.capitalize()}**: {title} Β· {layout} layout Β· " | |
| f"{n_sections} section{'s' if n_sections != 1 else ''} Β· " | |
| f"{n_comps} component{'s' if n_comps != 1 else ''}" | |
| ] | |
| if reasoning: | |
| lines.append(f"_{reasoning}_") | |
| return "\n\n".join(lines) | |
| def _friendly_error(raw_error: str) -> str: | |
| lower = raw_error.lower() | |
| if "blocked import" in lower: | |
| # Extract which module was blocked for a more helpful message | |
| import re | |
| m = re.search(r"blocked import[^:]*:\s*(.+)", raw_error, re.IGNORECASE) | |
| detail = m.group(1).strip() if m else "" | |
| suffix = f" ({detail})" if detail else "" | |
| return ( | |
| f"The generated code used a library that isn't available in the sandbox{suffix}. " | |
| "Try rephrasing β e.g. 'show a plotly scatter chart' instead of 'use sklearn KMeans'." | |
| ) | |
| if "nameerror" in lower: | |
| return "The generated code referenced something that doesn't exist. I tried to fix it but couldn't." | |
| if "syntaxerror" in lower: | |
| return "The generated code had a syntax problem I couldn't auto-repair." | |
| if "importerror" in lower or "modulenot" in lower: | |
| return "The generated code tried to import something unavailable." | |
| if "timeout" in lower or "timed out" in lower: | |
| return "The generated page took too long to start. Try a simpler request." | |
| return "The generated page had an error I couldn't fix automatically. Please try rephrasing." | |
| def _fail(state: dict, message: str, spec=None, code: str = "") -> dict: | |
| from agent.memory import append_assistant | |
| state = append_assistant(state, message) | |
| state["_reply"] = message | |
| state["last_error"] = message | |
| if spec is not None: | |
| state["current_spec"] = spec | |
| if code: | |
| state["current_code"] = code | |
| return state | |
| def _trace(prompt, code, spec, valid, exec_ok, exec_err, device, model_id): | |
| try: | |
| from storage.trace_logger import log_turn | |
| log_turn( | |
| prompt = prompt, | |
| raw_output = code, | |
| parsed_spec = spec, | |
| valid = valid, | |
| exec_success = exec_ok, | |
| exec_error = exec_err, | |
| device = device, | |
| model_id = model_id, | |
| ) | |
| except Exception: | |
| logger.exception("trace logging failed") | |