Snap2Sim / docs /features /vision-prompt-roles.md
jasondo
Finalize docs and archive prompts
7006f4b
|
Raw
History Blame Contribute Delete
8.16 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

Vision Prompt Roles: Split System and User Prompt

Question being answered

Is it better to split the analysis portion and the generation portion into different system prompts and user prompts? Would this help the model interpret the input and produce better output?

Short answer: Yes, split β€” but split by chat role inside the single analysis call, not into two model calls. The "generation" step is not a model step in this codebase, so the only useful split is system-vs-user within the one vision call. Wiring that up is a small, safe change that aligns the prompt with how the instruct/reasoning model was trained.


Findings

Finding 1 β€” There is only one model step; "generation" is deterministic

The pipeline is:

image -> [MODEL] vision analysis -> validated JSON -> [NO MODEL] Three.js scene
  • Scene rendering is deterministic browser-side Three.js built from the validated analysis JSON (AGENTS.md:34-35, AGENTS.md:196-198).
  • InferenceClient.generate_scene (snap2sim/backend.py:47-51) does no model inference β€” it validates the analysis and picks a render_mode. Model- authored scene HTML/JS was intentionally removed (SECURITY.md:96-97).

Implication: "split analysis and generation into different prompts" cannot mean two model calls. There is no generation model call to separate, and adding a second round-trip would double an already slow path (~35s observed, 300s timeout at modal_app.py:392) for zero quality gain, since the scene is deterministic. Do not introduce a second inference step.

Finding 2 β€” The system prompt is currently dead code

  • VISION_SYSTEM_PROMPT (snap2sim/prompts.py:5-10) is defined but never imported or used. modal_app.py imports only build_vision_prompt (modal_app.py:20).
  • The real call, run_llamacpp_prompt (modal_app.py:291-334), passes the prompt through a single -p flag (modal_app.py:308-309). There is no system message.
  • Net effect: the model receives one large user turn that mixes four different concerns β€” role framing, the analytical task, the full output schema, and field-by-field formatting rules (build_vision_prompt, snap2sim/prompts.py:12-70).

So today there is effectively no system/user separation, and the one "system" string we wrote is doing nothing.

Finding 3 β€” The runtime supports a real role split

  • llama-mtmd-cli accepts a separate system message via -sys alongside the user prompt -p, and applies the model's chat template. (Confirmed against llama.cpp mtmd-cli usage.)
  • The model is unsloth/NVIDIA-Nemotron-3-Nano-...-Reasoning-GGUF (AGENTS.md:20), an instruct/reasoning model. Such models are trained to treat the system turn as persistent role + constraints and the user turn as the immediate request. Collapsing everything into the user turn is off-distribution and dilutes the per-request ask.

Why a role split should help interpretation and output

  1. Aligns with training. Stable behavior/output-contract belongs in system; the moment-to-moment ask belongs in user. The model already expects this shape.
  2. Sharpens the ask. The user turn becomes short and image-focused ("Analyze the component in this photo …") instead of being buried under ~40 lines of schema rules.
  3. Separates invariants from the request. The schema, shape/motion vocabulary, and hard rules are constant across every image; they read as policy in system, not as part of this request.
  4. Stops wasting the role channel. We already wrote a system prompt; right now it is ignored.

This is a low-risk change: it does not touch the schema, validator, coercion (snap2sim/schema.py, snap2sim/model_io.py), or the deterministic renderer.


Recommendation

Adopt a system/user role split within the single vision call. Concretely:

  • System message = the invariant output contract:
    • role + reasoning frame (it is a reasoning model; brief reasoning then JSON),
    • the "emit exactly one JSON object, no markdown" rule,
    • the JSON skeleton,
    • the shape -> use-for and motion -> use-for vocabulary guide,
    • the hard field rules (size: [x,y,z], numeric axis vectors, 2–6 parts, no radius/height/length/width/depth).
  • User message = only the per-image ask: "Analyze the hardware component in this photo as a cutaway mechanism and return the analysis JSON." (The image is attached via --image.)

Keep it one inference call. Keep temperature, token, context, and timeout budgets as-is (modal_app.py:388-394) β€” this is a prompt-structure change, not a budget change.


Implementation plan (completed)

  1. snap2sim/prompts.py

    • Repurpose VISION_SYSTEM_PROMPT to hold the full invariant contract (role + JSON-only rule + skeleton + shape/motion vocabulary guide + hard field rules) currently living inside build_vision_prompt.
    • Reduce build_vision_prompt() to the short per-image ask only. Consider renaming it build_vision_user_prompt() (keep a back-compat alias if any diagnostic entrypoint imports the old name).
    • Optionally add build_vision_messages() returning (system, user) so call sites have one source of truth.
  2. snap2sim/prompts.py (smoke test parity, optional)

    • The smoke-test prompt is a separate inline string (modal_app.py:211-234). Leave functionally as-is, but it can reuse the same system message for realism. Not required for the demo.
  3. modal_app.py

    • In run_llamacpp_prompt (:291-334), add an optional system_prompt: str | None = None parameter and append -sys <system_prompt> to cmd when provided. Verify the exact flag name against the deployed llama-mtmd-cli build before relying on it (-sys); if the installed build does not accept it, fall back to prepending the system text to -p so behavior never regresses.
    • In analyze_image_llamacpp_payload (:385-400), pass the new system message + the short user prompt.
    • Update the diagnostic entrypoints that build the prompt (run_analysis_raw_check near :489, run_analysis_endpoint_check, remote check near :512) to use the same two-part prompt so diagnostics match production.
  4. Verify before/after (no regressions, measured improvement)

    • Local: schema/parser checks + FastAPI TestClient for /, /analyze_image, /generate_scene still pass.
    • Modal: run run_analysis_raw_check and run_analysis_endpoint_check against the synthetic target image; confirm strict JSON still parses and latency is comparable. Compare parse success / field quality vs. the current single--p prompt on a few representative real photos before committing the prompt change.
    • Confirm the flag actually took effect (e.g. inspect that -sys is honored, not silently ignored) before trusting the split.
  5. Then deploy following the normal path (Modal deploy of analyze_image_llamacpp, GitHub main push, GitHub->HF sync, and Space verification), per the existing workflow in AGENTS.md.

Final submission note, 2026-06-15: this prompt-role split shipped before the public build-small-hackathon/Snap2Sim submission.


Explicitly NOT recommended

  • Two separate model calls (an "analysis" call feeding a "generation" call). There is no model generation step; the scene is deterministic. This would only add latency and a failure mode.
  • Re-introducing model-authored scene HTML/JS. Prohibited by SECURITY.md:96-97 and AGENTS.md:35.

Open question for the user

The recommendation assumes you meant "should the prompt be structured into system vs user roles" (yes), not "should the app make two separate model requests" (no β€” there is no model generation step). If you specifically wanted the model to also generate the scene/animation (re-adding a model generation call), that conflicts with the current deterministic-renderer security decision and should be discussed separately before implementing.