forked from enderofwings/NexusOS
refactor(preview): simplify compile and validation paths
Load Sucrase only when a JSX/TSX preview is opened, remove the hand-written transform, and leave subjective render evaluation to the reader while retaining structural fence validation.
This commit is contained in:
+4
-113
@@ -139,7 +139,6 @@ async def _normalize_to_async_generator(maybe_iterable) -> AsyncGenerator[str, N
|
||||
pending_approvals: Dict[str, Dict[str, Any]] = {}
|
||||
_APPROVAL_TIMEOUT = 300 # seconds; a timeout is treated as "deny all"
|
||||
|
||||
|
||||
def _as_tool_calls(obj) -> list:
|
||||
"""Normalize a parsed JSON value into Ollama-style tool_calls entries."""
|
||||
if isinstance(obj, list):
|
||||
@@ -208,27 +207,6 @@ def _coerce_tool_calls(msg: dict, allowed_names: set[str] | None = None) -> list
|
||||
return []
|
||||
|
||||
|
||||
_VISUAL_HINTS = _tools._RENDER_HINTS
|
||||
|
||||
|
||||
def _render_nudge_text() -> str:
|
||||
"""The one retry given to a model that ignored render_preview on a visual ask.
|
||||
|
||||
It arrives as a *user* turn, which means the model answers whatever it says.
|
||||
Earlier wording pointed at "THIS user request" — a thing the model cannot
|
||||
see — and offered "if the user's term is unclear, ask them to clarify". It
|
||||
took both: two transcripts answered with "please provide the user's request
|
||||
for the rendering" and nothing else. So this says only what to do next, with
|
||||
no dangling reference and no escape hatch, and it names the languages the
|
||||
render window actually supports rather than a hardcoded pair."""
|
||||
return (
|
||||
f"Use the render_preview tool now. Send complete {_tools._lang_prose()} "
|
||||
f"markup drawn on a {_tools._STAGE_W}x{_tools._STAGE_H} stage, with the "
|
||||
"values computed into an array and plotted point by point. Do not write "
|
||||
"a ``` fence yourself."
|
||||
)
|
||||
|
||||
|
||||
def _strip_internal_turns(messages: list) -> list:
|
||||
"""Flatten tool-loop messages for the final, tool-free streaming turn.
|
||||
|
||||
@@ -236,15 +214,13 @@ def _strip_internal_turns(messages: list) -> list:
|
||||
the tools schema isn't re-sent. Their content must not go with them, though:
|
||||
search/memory/document results are the reason the loop ran. Preserve those
|
||||
results as an explicitly untrusted user-context turn immediately before the
|
||||
real request, while dropping assistant tool-call envelopes and the synthetic
|
||||
render nudge. Keeping the real request last also prevents the model from
|
||||
answering the nudge or treating a tool result as the user's question."""
|
||||
nudge = _render_nudge_text()
|
||||
real request, while dropping assistant tool-call envelopes. Keeping the real
|
||||
request last prevents the model from treating a tool result as the user's
|
||||
question."""
|
||||
kept = [
|
||||
m for m in messages
|
||||
if m.get("role") != "tool"
|
||||
and not m.get("tool_calls")
|
||||
and m.get("content") != nudge
|
||||
]
|
||||
results = [
|
||||
str(m.get("content") or "")
|
||||
@@ -270,35 +246,6 @@ def _strip_internal_turns(messages: list) -> list:
|
||||
return kept
|
||||
|
||||
|
||||
def _should_nudge_render(messages: list, tool_schemas: list | None) -> bool:
|
||||
"""True when render_preview is available, unused, and the user asked for a visual."""
|
||||
names = {
|
||||
(s.get("function") or {}).get("name")
|
||||
for s in (tool_schemas or [])
|
||||
if isinstance(s, dict)
|
||||
}
|
||||
if "render_preview" not in names:
|
||||
return False
|
||||
for m in messages:
|
||||
if m.get("role") == "assistant":
|
||||
for c in (m.get("tool_calls") or []):
|
||||
if (c.get("function") or {}).get("name") == "render_preview":
|
||||
return False
|
||||
if m.get("role") == "tool":
|
||||
try:
|
||||
body = _json.loads(m.get("content") or "")
|
||||
if isinstance(body, dict) and ("fence" in body or "issues" in body):
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
user = ""
|
||||
for m in reversed(messages):
|
||||
if m.get("role") == "user":
|
||||
user = (m.get("content") or "").lower()
|
||||
break
|
||||
return _tools.wants_render_preview(user)
|
||||
|
||||
|
||||
async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, num_gpu,
|
||||
conversation_id="", policy="allow"):
|
||||
"""Let the model call tools before the final streamed answer.
|
||||
@@ -317,8 +264,6 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
# Let the UI show activity immediately — the first tool-turn is a full
|
||||
# non-stream generation and can sit silent for a long time otherwise.
|
||||
yield "__status__tools"
|
||||
nudged_render = False
|
||||
render_rejects = 0
|
||||
allowed_names = {
|
||||
(schema.get("function") or {}).get("name")
|
||||
for schema in (tool_schemas or [])
|
||||
@@ -333,11 +278,6 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
break # None/error or no tool support -> fall back to plain stream
|
||||
calls = _coerce_tool_calls(msg, allowed_names)
|
||||
if not calls:
|
||||
# One retry: small models often skip render_preview on visual asks.
|
||||
if not nudged_render and _should_nudge_render(messages, tool_schemas):
|
||||
nudged_render = True
|
||||
messages.append({"role": "user", "content": _render_nudge_text()})
|
||||
continue
|
||||
break
|
||||
# Normalize content-JSON tool calls into the shape later turns expect.
|
||||
if not msg.get("tool_calls"):
|
||||
@@ -380,50 +320,19 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
continue
|
||||
yield f"__status__{name}"
|
||||
call_args = fn.get("arguments")
|
||||
# Tell render_preview how many times it has already turned this
|
||||
# model away. It withholds its scaffold on a first rejection - a
|
||||
# complete, styled demo handed to a struggling model gets pasted
|
||||
# rather than adapted, and then persists into the conversation as a
|
||||
# template for later requests.
|
||||
if name == "render_preview" and isinstance(call_args, dict):
|
||||
call_args = {**call_args, "_attempt": render_rejects}
|
||||
result = await _tools.dispatch(name, call_args)
|
||||
messages.append({"role": "tool", "content": result})
|
||||
# Cap render_preview reject loops — each retry is another full
|
||||
# non-stream generation and looks like the UI is "stuck thinking".
|
||||
if name == "render_preview":
|
||||
try:
|
||||
body = _json.loads(result)
|
||||
except Exception:
|
||||
body = {}
|
||||
if isinstance(body, dict) and body.get("ok") is False:
|
||||
render_rejects += 1
|
||||
if render_rejects >= 2:
|
||||
stop_after = True
|
||||
elif isinstance(body, dict) and body.get("ok") is True:
|
||||
if isinstance(body, dict) and body.get("ok") is True:
|
||||
# Good fence in hand — let the model write the reply next.
|
||||
stop_after = True
|
||||
if stop_after:
|
||||
break
|
||||
|
||||
|
||||
def _last_ok_render_fence(messages: list) -> tuple[str | None, dict]:
|
||||
"""Return (fence, tool_payload) from the latest successful render_preview."""
|
||||
for m in reversed(messages or []):
|
||||
if m.get("role") != "tool":
|
||||
continue
|
||||
try:
|
||||
body = _json.loads(m.get("content") or "")
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(body, dict) or not body.get("ok"):
|
||||
continue
|
||||
fence = str(body.get("fence") or "").strip()
|
||||
if fence.startswith("```"):
|
||||
return fence, body
|
||||
return None, {}
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Streaming implementation
|
||||
# -------------------------
|
||||
@@ -471,24 +380,6 @@ async def stream_chat_response(
|
||||
except Exception:
|
||||
_logger.exception("tool loop failed; streaming without tools")
|
||||
|
||||
# If render_preview already produced a validated fence, emit it ourselves.
|
||||
# Small models often "paste" a rewritten, broken copy that never runs in Preview.
|
||||
forced_fence, render_meta = _last_ok_render_fence(messages)
|
||||
if forced_fence:
|
||||
label = render_meta.get("title") or "Interactive preview"
|
||||
reply = f"{label}:\n\n{forced_fence}\n"
|
||||
_logger.info(
|
||||
"stream_chat_response: emitting validated render fence (chars=%d)",
|
||||
len(forced_fence),
|
||||
)
|
||||
_synapse_trace(f"\n── TURN [{model} | render fence] {'─' * 30}\n")
|
||||
_synapse_trace(f"USR: {user_message}\n{'─' * 50}\n")
|
||||
_synapse_trace(reply.replace("\n", " ")[:500] + "\n")
|
||||
step = 64
|
||||
for i in range(0, len(reply), step):
|
||||
yield reply[i:i + step]
|
||||
return
|
||||
|
||||
messages = _strip_internal_turns(messages)
|
||||
|
||||
_logger.info("stream_chat_response: starting stream (model=%s, turns=%d, timeout=%s)", model, len(messages), timeout)
|
||||
|
||||
Reference in New Issue
Block a user